52个文件已修改
8个文件已删除
1,791个文件已添加
New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| | | <plist version="1.0"> |
| | | <dict> |
| | | <key>IDEDidComputeMac32BitWarning</key> |
| | | <true/> |
| | | </dict> |
| | | </plist> |
New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <Scheme |
| | | LastUpgradeVersion = "1530" |
| | | version = "1.7"> |
| | | <BuildAction |
| | | parallelizeBuildables = "YES" |
| | | buildImplicitDependencies = "YES" |
| | | buildArchitectures = "Automatic"> |
| | | <BuildActionEntries> |
| | | <BuildActionEntry |
| | | buildForTesting = "YES" |
| | | buildForRunning = "YES" |
| | | buildForProfiling = "YES" |
| | | buildForArchiving = "YES" |
| | | buildForAnalyzing = "YES"> |
| | | <BuildableReference |
| | | BuildableIdentifier = "primary" |
| | | BlueprintIdentifier = "130278252BFD957100DDCE81" |
| | | BuildableName = "DolphinEnglishLearnStudent.app" |
| | | BlueprintName = "DolphinEnglishLearnStudent" |
| | | ReferencedContainer = "container:DolphinEnglishLearnStudent.xcodeproj"> |
| | | </BuildableReference> |
| | | </BuildActionEntry> |
| | | </BuildActionEntries> |
| | | </BuildAction> |
| | | <TestAction |
| | | buildConfiguration = "Debug" |
| | | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" |
| | | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" |
| | | shouldUseLaunchSchemeArgsEnv = "YES" |
| | | shouldAutocreateTestPlan = "YES"> |
| | | </TestAction> |
| | | <LaunchAction |
| | | buildConfiguration = "Debug" |
| | | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" |
| | | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" |
| | | launchStyle = "0" |
| | | useCustomWorkingDirectory = "NO" |
| | | ignoresPersistentStateOnLaunch = "NO" |
| | | debugDocumentVersioning = "YES" |
| | | debugServiceExtension = "internal" |
| | | allowLocationSimulation = "YES"> |
| | | <BuildableProductRunnable |
| | | runnableDebuggingMode = "0"> |
| | | <BuildableReference |
| | | BuildableIdentifier = "primary" |
| | | BlueprintIdentifier = "130278252BFD957100DDCE81" |
| | | BuildableName = "DolphinEnglishLearnStudent.app" |
| | | BlueprintName = "DolphinEnglishLearnStudent" |
| | | ReferencedContainer = "container:DolphinEnglishLearnStudent.xcodeproj"> |
| | | </BuildableReference> |
| | | </BuildableProductRunnable> |
| | | </LaunchAction> |
| | | <ProfileAction |
| | | buildConfiguration = "Release" |
| | | shouldUseLaunchSchemeArgsEnv = "YES" |
| | | savedToolIdentifier = "" |
| | | useCustomWorkingDirectory = "NO" |
| | | debugDocumentVersioning = "YES"> |
| | | <BuildableProductRunnable |
| | | runnableDebuggingMode = "0"> |
| | | <BuildableReference |
| | | BuildableIdentifier = "primary" |
| | | BlueprintIdentifier = "130278252BFD957100DDCE81" |
| | | BuildableName = "DolphinEnglishLearnStudent.app" |
| | | BlueprintName = "DolphinEnglishLearnStudent" |
| | | ReferencedContainer = "container:DolphinEnglishLearnStudent.xcodeproj"> |
| | | </BuildableReference> |
| | | </BuildableProductRunnable> |
| | | </ProfileAction> |
| | | <AnalyzeAction |
| | | buildConfiguration = "Debug"> |
| | | </AnalyzeAction> |
| | | <ArchiveAction |
| | | buildConfiguration = "Debug" |
| | | revealArchiveInOrganizer = "YES"> |
| | | </ArchiveAction> |
| | | </Scheme> |
New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <Workspace |
| | | version = "1.0"> |
| | | <FileRef |
| | | location = "group:DolphinEnglishLearnStudent.xcodeproj"> |
| | | </FileRef> |
| | | <FileRef |
| | | location = "group:Pods/Pods.xcodeproj"> |
| | | </FileRef> |
| | | </Workspace> |
New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| | | <plist version="1.0"> |
| | | <dict> |
| | | <key>IDEDidComputeMac32BitWarning</key> |
| | | <true/> |
| | | </dict> |
| | | </plist> |
| | |
| | | |
| | | class AppDelegate: UIResponder, UIApplicationDelegate { |
| | | |
| | | var window: UIWindow? |
| | | var window: UIWindow? |
| | | |
| | | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { |
| | | // Override point for customization after application launch. |
| | | sleep(2) |
| | | return true |
| | | } |
| | | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { |
| | | // Override point for customization after application launch. |
| | | sleep(2) |
| | | return true |
| | | } |
| | | |
| | | // MARK: UISceneSession Lifecycle |
| | | // MARK: UISceneSession Lifecycle |
| | | |
| | | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { |
| | | // Called when a new scene session is being created. |
| | | // Use this method to select a configuration to create the new scene with. |
| | | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) |
| | | } |
| | | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { |
| | | // Called when a new scene session is being created. |
| | | // Use this method to select a configuration to create the new scene with. |
| | | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) |
| | | } |
| | | |
| | | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) { |
| | | // Called when the user discards a scene session. |
| | | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. |
| | | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. |
| | | } |
| | | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) { |
| | | // Called when the user discards a scene session. |
| | | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. |
| | | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. |
| | | } |
| | | |
| | | func application(_ application: UIApplication, handleOpen url: URL) -> Bool { |
| | | return WXApi.handleOpen(url, delegate: self) |
| | | } |
| | | func application(_ application: UIApplication, handleOpen url: URL) -> Bool { |
| | | return WXApi.handleOpen(url, delegate: self) |
| | | } |
| | | |
| | | func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { |
| | | return WXApi.handleOpen(url, delegate: self) |
| | | } |
| | | func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { |
| | | return WXApi.handleOpen(url, delegate: self) |
| | | } |
| | | } |
| | | |
| | | extension AppDelegate:WXApiDelegate{ |
| | | func onReq(_ req: BaseReq) { |
| | | func onReq(_ req: BaseReq) { |
| | | |
| | | } |
| | | } |
| | | |
| | | func onResp(_ resp: BaseResp) { |
| | | func onResp(_ resp: BaseResp) { |
| | | |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "bg_abc.png", |
| | | "idiom" : "ipad", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "bg_abc@2x.png", |
| | | "idiom" : "ipad", |
| | | "filename" : "bg_abc.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "bg_abc@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | | ], |
| | | "info" : { |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "bg_card.png", |
| | | "idiom" : "ipad", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "bg_card@2x.png", |
| | | "idiom" : "ipad", |
| | | "filename" : "bg_card.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "bg_card@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | | ], |
| | | "info" : { |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "bg_login.png", |
| | | "idiom" : "ipad", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "bg_login@2x.png", |
| | | "idiom" : "ipad", |
| | | "filename" : "bg_login.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "bg_login@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | | ], |
| | | "info" : { |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "wecom-temp-67726-6439a5b9afc49824ac67b405478482e0.png", |
| | | "idiom" : "ipad", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "wecom-temp-67726-6439a5b9afc49824ac67b405478482e01.png", |
| | | "idiom" : "ipad", |
| | | "filename" : "wecom-temp-67726-6439a5b9afc49824ac67b405478482e0.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "wecom-temp-67726-6439a5b9afc49824ac67b405478482e01.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | | ], |
| | | "info" : { |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "btn_add_un.png", |
| | | "idiom" : "ipad", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "btn_add_un@2x.png", |
| | | "idiom" : "ipad", |
| | | "filename" : "btn_add_un.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "btn_add_un@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | | ], |
| | | "info" : { |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "btn_radio.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "btn_radio@2x.png", |
| | | "filename" : "btn_radio.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "btn_radio@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "btn_radio_u.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "btn_radio_u@2x.png", |
| | | "filename" : "btn_radio_u.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "btn_radio_u@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "btn_refresh.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "btn_refresh@2x.png", |
| | | "filename" : "btn_refresh.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "btn_refresh@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "placeH.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "placeH@2x.png", |
| | | "filename" : "placeH.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "placeH@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "icon_answer.png", |
| | | "filename" : "zuiba-xuanzhong@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "icon_answer@2x.png", |
| | | "filename" : "zuiba-xuanzhong@3x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "icon_play.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "icon_play@2x.png", |
| | | "filename" : "play@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "play@3x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "icon_play_1.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "icon_play_1@2x.png", |
| | | "filename" : "Group 4@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "Group 4@3x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "icon_playing.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "icon_playing@2x.png", |
| | | "filename" : "icon_playing.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "icon_playing@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "icon_poker.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "icon_poker@2x.png", |
| | | "filename" : "icon_poker.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "icon_poker@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "icon_question.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "icon_question@2x.png", |
| | | "filename" : "wenhao (1)@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "wenhao (1)@3x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "icon_share.png", |
| | | "idiom" : "ipad", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "icon_share@2x.png", |
| | | "idiom" : "ipad", |
| | | "filename" : "icon_share.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "icon_share@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | | ], |
| | | "info" : { |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "icon_success.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "icon_success@2x.png", |
| | | "filename" : "icon_success.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "icon_success@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "icon_success_small.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "icon_success_small@2x.png", |
| | | "filename" : "icon_success_small.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "icon_success_small@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "icon_vip.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "icon_vip@2x.png", |
| | | "filename" : "icon_vip.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "icon_vip@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "icon_waring.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "icon_waring@2x.png", |
| | | "filename" : "icon_waring.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "icon_waring@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "icon_waring_small.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "icon_waring_small@2x.png", |
| | | "filename" : "icon_waring_small.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "icon_waring_small@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "share_wx.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "share_wx@2x.png", |
| | | "filename" : "share_wx.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "share_wx@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | { |
| | | "images" : [ |
| | | { |
| | | "filename" : "share_wxFri.png", |
| | | "idiom" : "universal", |
| | | "scale" : "1x" |
| | | }, |
| | | { |
| | | "filename" : "share_wxFri@2x.png", |
| | | "filename" : "share_wxFri.png", |
| | | "idiom" : "universal", |
| | | "scale" : "2x" |
| | | }, |
| | | { |
| | | "filename" : "share_wxFri@2x.png", |
| | | "idiom" : "universal", |
| | | "scale" : "3x" |
| | | } |
| | |
| | | |
| | | class BaseVC: UIViewController { |
| | | |
| | | var disposeBag:DisposeBag! |
| | | let refreshStatus = BehaviorSubject(value: RefreshStatus.others) |
| | | var disposeBag:DisposeBag! |
| | | let refreshStatus = BehaviorSubject(value: RefreshStatus.others) |
| | | |
| | | var yy_popBlock:(() -> Void)? |
| | | open var nav_back_img:UIImage = UIImage.init(named: "btn_back") ?? UIImage.init() { |
| | | didSet { |
| | | let btn = navigationItem.leftBarButtonItem?.customView as! UIButton |
| | | btn.setImage(nav_back_img, for: .normal) |
| | | } |
| | | } |
| | | var yy_popBlock:(() -> Void)? |
| | | open var nav_back_img:UIImage = UIImage.init(named: "btn_back") ?? UIImage.init() { |
| | | didSet { |
| | | let btn = navigationItem.leftBarButtonItem?.customView as! UIButton |
| | | btn.setImage(nav_back_img, for: .normal) |
| | | } |
| | | } |
| | | |
| | | override func viewWillAppear(_ animated: Bool) { |
| | | super.viewWillAppear(animated) |
| | | navigationController?.delegate?.navigationController?(navigationController!, willShow: self, animated: true) |
| | | } |
| | | override func viewWillAppear(_ animated: Bool) { |
| | | super.viewWillAppear(animated) |
| | | navigationController?.delegate?.navigationController?(navigationController!, willShow: self, animated: true) |
| | | } |
| | | |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | |
| | | |
| | | disposeBag = DisposeBag() |
| | | setUI() |
| | | setRx() |
| | | setData() |
| | | disposeBag = DisposeBag() |
| | | setUI() |
| | | setRx() |
| | | setData() |
| | | |
| | | if navigationController?.viewControllers.count ?? 0 > 1{ |
| | | let backButton = QMUIButton(type: .custom) |
| | | backButton.setImage(UIImage(named: "btn_back"), for: .normal) |
| | | backButton.setTitle(self.title, for: .normal) |
| | | backButton.setTitleColor(.black.withAlphaComponent(0.81), for: .normal) |
| | | backButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium) |
| | | backButton.imagePosition = .left |
| | | backButton.spacingBetweenImageAndTitle = 35 |
| | | backButton.addTarget(self, action: #selector(backItemEvent), for: .touchUpInside) |
| | | navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton) |
| | | } |
| | | if navigationController?.viewControllers.count ?? 0 > 1{ |
| | | let backButton = QMUIButton(type: .custom) |
| | | backButton.setImage(UIImage(named: "btn_back"), for: .normal) |
| | | backButton.setTitle(self.title, for: .normal) |
| | | backButton.setTitleColor(.black.withAlphaComponent(0.81), for: .normal) |
| | | backButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium) |
| | | backButton.imagePosition = .left |
| | | backButton.spacingBetweenImageAndTitle = 35 |
| | | backButton.addTarget(self, action: #selector(backItemEvent), for: .touchUpInside) |
| | | navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton) |
| | | } |
| | | |
| | | if !self.isKind(of: HomeVC.self) && !self.isKind(of: HomeListenSubVC.self) && !self.isKind(of: HomeListenFight_lesson_1_VC.self) && !self.isKind(of: HomeListenFight_lesson_2_VC.self) && !self.isKind(of: HomeListenFight_lesson_3_VC.self) && !self.isKind(of: HomeListenFight_lesson_4_VC.self) && !self.isKind(of: HomeListenFight_lesson_5_VC.self) && !self.isKind(of: HomeListenGame_1_VC.self) && !self.isKind(of: HomeListenGame_2_VC.self) && !self.isKind(of: HomeListenStory_1_VC.self) && !self.isKind(of: HomeListenStory_2_VC.self) && !self.isKind(of: LoginVC.self){ |
| | | let titleV = UIView() |
| | | // titleV.bounds = CGRect(x: 0, y: 0, width: 156, height: 24) |
| | | titleV.sizeToFit() |
| | | let imgV = UIImageView(image: UIImage(named: "bg_logo")) |
| | | imgV.contentMode = .scaleAspectFit |
| | | titleV.addSubview(imgV) |
| | | imgV.snp.makeConstraints { make in |
| | | make.edges.equalToSuperview() |
| | | } |
| | | if !self.isKind(of: HomeVC.self) && !self.isKind(of: HomeListenSubVC.self) && !self.isKind(of: HomeListenFight_lesson_1_VC.self) && !self.isKind(of: HomeListenFight_lesson_2_VC.self) && !self.isKind(of: HomeListenFight_lesson_3_VC.self) && !self.isKind(of: HomeListenFight_lesson_4_VC.self) && !self.isKind(of: HomeListenFight_lesson_5_VC.self) && !self.isKind(of: HomeListenGame_1_VC.self) && !self.isKind(of: HomeListenGame_2_VC.self) && !self.isKind(of: HomeListenStory_1_VC.self) && !self.isKind(of: HomeListenStory_2_VC.self) && !self.isKind(of: LoginVC.self){ |
| | | let titleV = UIView() |
| | | // titleV.bounds = CGRect(x: 0, y: 0, width: 156, height: 24) |
| | | titleV.sizeToFit() |
| | | let imgV = UIImageView(image: UIImage(named: "bg_logo")) |
| | | imgV.contentMode = .scaleAspectFit |
| | | titleV.addSubview(imgV) |
| | | imgV.snp.makeConstraints { make in |
| | | make.edges.equalToSuperview() |
| | | } |
| | | |
| | | view.addSubview(titleV) |
| | | titleV.snp.makeConstraints { make in |
| | | make.center.equalToSuperview() |
| | | } |
| | | view.addSubview(titleV) |
| | | titleV.snp.makeConstraints { make in |
| | | make.center.equalToSuperview() |
| | | } |
| | | |
| | | navigationItem.titleView = titleV |
| | | } |
| | | } |
| | | navigationItem.titleView = titleV |
| | | } |
| | | } |
| | | |
| | | func setRx(){ |
| | | } |
| | | func setRx(){ |
| | | } |
| | | |
| | | func setUI(){ |
| | | view.backgroundColor = Config.ThemeBGColor |
| | | func setUI(){ |
| | | view.backgroundColor = Config.ThemeBGColor |
| | | |
| | | } |
| | | } |
| | | |
| | | func setData(){ |
| | | func setData(){ |
| | | |
| | | } |
| | | } |
| | | |
| | | func refreshUI(){} |
| | | func refreshUI(){} |
| | | |
| | | func push(vc:UIViewController){ |
| | | vc.hidesBottomBarWhenPushed = true |
| | | navigationController?.pushViewController(vc, animated: true) |
| | | } |
| | | func push(vc:UIViewController){ |
| | | vc.hidesBottomBarWhenPushed = true |
| | | navigationController?.pushViewController(vc, animated: true) |
| | | } |
| | | |
| | | override var preferredStatusBarStyle: UIStatusBarStyle{ |
| | | return .lightContent |
| | | } |
| | | override var preferredStatusBarStyle: UIStatusBarStyle{ |
| | | return .lightContent |
| | | } |
| | | |
| | | @objc fileprivate func backItemEvent() { |
| | | // 拦截pop事件 |
| | | if (yy_popBlock != nil) { |
| | | yy_popBlock?() |
| | | return |
| | | } |
| | | navigationController?.popViewController(animated: true) |
| | | } |
| | | @objc fileprivate func backItemEvent() { |
| | | // 拦截pop事件 |
| | | if (yy_popBlock != nil) { |
| | | yy_popBlock?() |
| | | return |
| | | } |
| | | navigationController?.popViewController(animated: true) |
| | | } |
| | | |
| | | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { |
| | | coordinator.animate(alongsideTransition: { [weak self] (context) in |
| | | let orient = UIApplication.shared.statusBarOrientation |
| | | switch orient { |
| | | case .landscapeLeft, .landscapeRight: |
| | | //横屏时禁止左拽滑出 |
| | | self?.navigationController?.interactivePopGestureRecognizer?.isEnabled = false |
| | | default: |
| | | //竖屏时允许左拽滑出 |
| | | self?.navigationController?.interactivePopGestureRecognizer?.isEnabled = true |
| | | } |
| | | }) |
| | | super.viewWillTransition(to: size, with: coordinator) |
| | | } |
| | | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { |
| | | coordinator.animate(alongsideTransition: { [weak self] (context) in |
| | | let orient = UIApplication.shared.statusBarOrientation |
| | | switch orient { |
| | | case .landscapeLeft, .landscapeRight: |
| | | //横屏时禁止左拽滑出 |
| | | self?.navigationController?.interactivePopGestureRecognizer?.isEnabled = false |
| | | default: |
| | | //竖屏时允许左拽滑出 |
| | | self?.navigationController?.interactivePopGestureRecognizer?.isEnabled = true |
| | | } |
| | | }) |
| | | super.viewWillTransition(to: size, with: coordinator) |
| | | } |
| | | |
| | | |
| | | deinit { |
| | | LogInfo(String(format: "%@ 已释放", NSStringFromClass(self.classForCoder).components(separatedBy: ".").last!)) |
| | | } |
| | | deinit { |
| | | LogInfo(String(format: "%@ 已释放", NSStringFromClass(self.classForCoder).components(separatedBy: ".").last!)) |
| | | } |
| | | |
| | | } |
| | |
| | | let ShareAppleKey = "af37e916cd2b4a0293e37ac405ba4f1c" |
| | | |
| | | var sceneDelegate:SceneDelegate? = { |
| | | var uiScreen:UIScene? |
| | | UIApplication.shared.connectedScenes.forEach { scenes in |
| | | uiScreen = scenes |
| | | } |
| | | return (uiScreen?.delegate as? SceneDelegate) |
| | | var uiScreen:UIScene? |
| | | UIApplication.shared.connectedScenes.forEach { scenes in |
| | | uiScreen = scenes |
| | | } |
| | | return (uiScreen?.delegate as? SceneDelegate) |
| | | }() |
| | | |
| | | |
| | | struct Config { |
| | | static let ThemeBGColor:UIColor = UIColor(hexStr: "#C3BFB3") |
| | | static let ThemeColor:UIColor = UIColor(hexStr: "#4195D3") |
| | | static let NavFontColor = UIColor.black.withAlphaComponent(0.8) |
| | | static let NavFont = UIFont.systemFont(ofSize: 15, weight: .medium) |
| | | static let ThemeBGColor:UIColor = UIColor(hexStr: "#C3BFB3") |
| | | static let ThemeColor:UIColor = UIColor(hexStr: "#4195D3") |
| | | static let NavFontColor = UIColor.black.withAlphaComponent(0.8) |
| | | static let NavFont = UIFont.systemFont(ofSize: 15, weight: .medium) |
| | | |
| | | static var RatioW:Double{get{return JQ_ScreenW / 810.0}} |
| | | static var RatioH:Double{get{return JQ_ScreenH / 1080.0}} |
| | | static var RatioW:Double{get{return JQ_ScreenW / 810.0}} |
| | | static var RatioH:Double{get{return JQ_ScreenH / 1080.0}} |
| | | } |
| | | |
| | | |
| | | func LogSuccess(_ items:Any...,separator:String=" ",file:String=#file,function:String=#function,line:Int=#line){ |
| | | #if DEBUG |
| | | if #available(iOS 14.0, *) { |
| | | let logger = Logger(subsystem: "English", category: function) |
| | | logger.error("\(items)") |
| | | }else{ |
| | | let file = (file as NSString).lastPathComponent.split(separator: ".").first!; |
| | | print("✅✅✅ SUCCESS: \(file) \(function) [Line: \(line)]: \(items)",separator); |
| | | } |
| | | if #available(iOS 14.0, *) { |
| | | let logger = Logger(subsystem: "English", category: function) |
| | | logger.error("\(items)") |
| | | }else{ |
| | | let file = (file as NSString).lastPathComponent.split(separator: ".").first!; |
| | | print("✅✅✅ SUCCESS: \(file) \(function) [Line: \(line)]: \(items)",separator); |
| | | } |
| | | |
| | | #endif |
| | | } |
| | | |
| | | func LogError(_ items:Any...,separator:String=" ",file:String=#file,function:String=#function,line:Int=#line){ |
| | | #if DEBUG |
| | | if #available(iOS 14.0, *) { |
| | | let logger = Logger(subsystem: "English", category: function) |
| | | logger.error("\(items)") |
| | | }else{ |
| | | let file = (file as NSString).lastPathComponent.split(separator: ".").first!; |
| | | print("❌❌❌ ERROR: \(file) \(function) [Line: \(line)]: \(items)",separator); |
| | | } |
| | | if #available(iOS 14.0, *) { |
| | | let logger = Logger(subsystem: "English", category: function) |
| | | logger.error("\(items)") |
| | | }else{ |
| | | let file = (file as NSString).lastPathComponent.split(separator: ".").first!; |
| | | print("❌❌❌ ERROR: \(file) \(function) [Line: \(line)]: \(items)",separator); |
| | | } |
| | | #endif |
| | | } |
| | | |
| | | func LogInfo(_ items:Any...,separator:String=" ",file:String=#file,function:String=#function,line:Int=#line){ |
| | | #if DEBUG |
| | | if #available(iOS 14.0, *) { |
| | | let logger = Logger(subsystem: "English", category: function) |
| | | logger.error("\(items)") |
| | | }else{ |
| | | let file = (file as NSString).lastPathComponent.split(separator: ".").first!; |
| | | print("⚠️⚠️⚠️INFO: \(file) \(function) [Line: \(line)]: \(items)",separator); |
| | | } |
| | | if #available(iOS 14.0, *) { |
| | | let logger = Logger(subsystem: "English", category: function) |
| | | logger.error("\(items)") |
| | | }else{ |
| | | let file = (file as NSString).lastPathComponent.split(separator: ".").first!; |
| | | print("⚠️⚠️⚠️INFO: \(file) \(function) [Line: \(line)]: \(items)",separator); |
| | | } |
| | | #endif |
| | | } |
| | | |
| | | func LogResponse(_ items:Any...,separator:String=" ",file:String=#file,function:String=#function,line:Int=#line){ |
| | | #if DEBUG |
| | | print("返回数据") |
| | | print(items); |
| | | print("返回数据") |
| | | print(items); |
| | | #endif |
| | | } |
| | | |
| | | //提示框 |
| | | func alert(msg: String) { |
| | | SVProgressHUD.showInfo(withStatus: msg) |
| | | SVProgressHUD.showInfo(withStatus: msg) |
| | | } |
| | | |
| | | func alertError(msg:String){ |
| | | SVProgressHUD.showError(withStatus: msg) |
| | | SVProgressHUD.showError(withStatus: msg) |
| | | } |
| | | |
| | | func alertSuccess(msg:String){ |
| | | SVProgressHUD.showSuccess(withStatus: msg) |
| | | SVProgressHUD.showSuccess(withStatus: msg) |
| | | } |
| | | |
| | | func showHUD(_ text:String? = nil){ |
| | | SVProgressHUD.show(withStatus: text) |
| | | SVProgressHUD.show(withStatus: text) |
| | | } |
| | | |
| | | func hiddenHUD(_ delay:TimeInterval? = nil){ |
| | | if delay != nil{ |
| | | SVProgressHUD.dismiss(withDelay: delay!) |
| | | }else{ |
| | | SVProgressHUD.dismiss() |
| | | } |
| | | if delay != nil{ |
| | | SVProgressHUD.dismiss(withDelay: delay!) |
| | | }else{ |
| | | SVProgressHUD.dismiss() |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | extension UIButton{ |
| | | public func openCountDown(_ t:Int = 59,defultTitle:String = "获取验证码",textColor:UIColor,unenableColor:UIColor){ |
| | | var time = t //倒计时时间 |
| | | let queue = DispatchQueue.main |
| | | let timer = DispatchSource.makeTimerSource(flags: [], queue: queue) |
| | | timer.schedule(wallDeadline: DispatchWallTime.now(), repeating: .seconds(1)); |
| | | timer.setEventHandler(handler: { |
| | | if time <= 0 { |
| | | timer.cancel() |
| | | DispatchQueue.main.async(execute: { |
| | | self.setTitle(defultTitle, for: .normal) |
| | | self.setTitleColor(textColor, for: .normal) |
| | | self.isUserInteractionEnabled = true |
| | | }); |
| | | }else { |
| | | self.setTitle("\(time)s", for: .normal) |
| | | self.setTitleColor(unenableColor, for: .normal) |
| | | self.isUserInteractionEnabled = false |
| | | } |
| | | time -= 1 |
| | | }); |
| | | timer.resume() |
| | | } |
| | | public func openCountDown(_ t:Int = 59,defultTitle:String = "获取验证码",textColor:UIColor,unenableColor:UIColor){ |
| | | var time = t //倒计时时间 |
| | | let queue = DispatchQueue.main |
| | | let timer = DispatchSource.makeTimerSource(flags: [], queue: queue) |
| | | timer.schedule(wallDeadline: DispatchWallTime.now(), repeating: .seconds(1)); |
| | | timer.setEventHandler(handler: { |
| | | if time <= 0 { |
| | | timer.cancel() |
| | | DispatchQueue.main.async(execute: { |
| | | self.setTitle(defultTitle, for: .normal) |
| | | self.setTitleColor(textColor, for: .normal) |
| | | self.isUserInteractionEnabled = true |
| | | }); |
| | | }else { |
| | | self.setTitle("\(time)s", for: .normal) |
| | | self.setTitleColor(unenableColor, for: .normal) |
| | | self.isUserInteractionEnabled = false |
| | | } |
| | | time -= 1 |
| | | }); |
| | | timer.resume() |
| | | } |
| | | } |
| | | |
| | | extension UIImage{ |
| | | var themeGreen:UIImage{ |
| | | return self.withTintColor(UIColor(hexStr: "#51aaed")) |
| | | } |
| | | } |
| | |
| | | import UserDefaultsStore |
| | | |
| | | struct LoginModel:HandyJSON{ |
| | | var token:LoginTokenModel? |
| | | var token:LoginTokenModel? |
| | | } |
| | | |
| | | struct LoginTokenModel:HandyJSON,Identifiable,Codable{ |
| | | static let idKey = \LoginTokenModel.id |
| | | var id: Int = 0 |
| | | static let idKey = \LoginTokenModel.id |
| | | var id: Int = 0 |
| | | |
| | | var access_token = "" |
| | | var expires_in = 0 |
| | | var request_time = 0 //请求时间 |
| | | var access_token = "" |
| | | var expires_in = 0 |
| | | var request_time = 0 //请求时间 |
| | | |
| | | private static let userInfo = UserDefaultsStore<LoginTokenModel>(uniqueIdentifier: "LoginTokenModel")! |
| | | private static let userInfo = UserDefaultsStore<LoginTokenModel>(uniqueIdentifier: "LoginTokenModel")! |
| | | |
| | | static func saveToken(_ model:LoginTokenModel){ |
| | | do{ |
| | | try LoginTokenModel.userInfo.save(model) |
| | | }catch{ |
| | | static func saveToken(_ model:LoginTokenModel){ |
| | | do{ |
| | | try LoginTokenModel.userInfo.save(model) |
| | | }catch{ |
| | | |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | static func isOverdue()->Bool{ |
| | | if let token = LoginTokenModel.getToken(){ |
| | | //过期时间(秒) |
| | | let overdueTimeval = token.expires_in * 60 + token.request_time |
| | | |
| | | if overdueTimeval < Int(Date().timeIntervalSince1970){ |
| | | return true |
| | | } |
| | | return false |
| | | } |
| | | return true |
| | | } |
| | | static func isOverdue()->Bool{ |
| | | if let token = LoginTokenModel.getToken(){ |
| | | //过期时间(秒) |
| | | let overdueTimeval = token.expires_in * 60 + token.request_time |
| | | |
| | | static func getToken()->LoginTokenModel?{ |
| | | return LoginTokenModel.userInfo.allObjects().first |
| | | } |
| | | if overdueTimeval < Int(Date().timeIntervalSince1970){ |
| | | return true |
| | | } |
| | | return false |
| | | } |
| | | return true |
| | | } |
| | | |
| | | static func clearToken(){ |
| | | LoginTokenModel.userInfo.deleteAll() |
| | | } |
| | | static func getToken()->LoginTokenModel?{ |
| | | return LoginTokenModel.userInfo.allObjects().first |
| | | } |
| | | |
| | | static func clearToken(){ |
| | | LoginTokenModel.userInfo.deleteAll() |
| | | } |
| | | } |
| | | |
| | | |
| | | struct RecommendModel:HandyJSON{ |
| | | var basicCount: Int = 0 |
| | | var coverImg: String = "" |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var detail: String = "" |
| | | var detailImg: String = "" |
| | | var disabled: Bool = false |
| | | var id: Int = 0 |
| | | var insertTime: String = "" |
| | | var integral: Int = 0 |
| | | var inventory: Int = 0 |
| | | var isDelete: Int = 0 |
| | | var name: String = "" |
| | | var price: Int = 0 |
| | | var surplus: Int = 0 |
| | | var total: Int = 0 |
| | | var type: Int = 0 |
| | | var typeIds: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var userCount: Int = 0 |
| | | var basicCount: Int = 0 |
| | | var coverImg: String = "" |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var detail: String = "" |
| | | var detailImg: String = "" |
| | | var disabled: Bool = false |
| | | var id: Int = 0 |
| | | var insertTime: String = "" |
| | | var integral: Int = 0 |
| | | var inventory: Int = 0 |
| | | var isDelete: Int = 0 |
| | | var name: String = "" |
| | | var price: Int = 0 |
| | | var surplus: Int = 0 |
| | | var total: Int = 0 |
| | | var type: Int = 0 |
| | | var typeIds: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var userCount: Int = 0 |
| | | } |
| | | |
| | | struct MarketModel:HandyJSON{ |
| | | var basicCount: Int = 0 |
| | | var coverImg: String = "" |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var detail: String = "" |
| | | var detailImg: String = "" |
| | | var disabled: Bool = false |
| | | var id: Int = 0 |
| | | var integral: Int = 0 |
| | | var inventory: Int = 0 |
| | | var isDelete: Int = 0 |
| | | var name: String = "" |
| | | var price: Double = 0 |
| | | var surplus: Int? |
| | | var total: Int = 0 |
| | | var type: Int = 0 |
| | | var typeIds: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var userCount: Int? |
| | | var basicCount: Int = 0 |
| | | var coverImg: String = "" |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var detail: String = "" |
| | | var detailImg: String = "" |
| | | var disabled: Bool = false |
| | | var id: Int = 0 |
| | | var integral: Int = 0 |
| | | var inventory: Int = 0 |
| | | var isDelete: Int = 0 |
| | | var name: String = "" |
| | | var price: Double = 0 |
| | | var surplus: Int? |
| | | var total: Int = 0 |
| | | var type: Int = 0 |
| | | var typeIds: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var userCount: Int? |
| | | } |
| | | |
| | | struct MarketTypeModel:HandyJSON,Hashable{ |
| | | var id = 0 |
| | | var name = "" |
| | | var id = 0 |
| | | var name = "" |
| | | } |
| | | |
| | | struct MarketDetailModel:HandyJSON{ |
| | | var exchangeNumber: Int = 0 |
| | | var good: MarketModel? |
| | | var goodTypes = [MarketTypeModel]() |
| | | var orderNumber: String = "" |
| | | var residueNumber:Int? |
| | | var recipient: MarketRecipientModel? |
| | | var exchangeNumber: Int = 0 |
| | | var good: MarketModel? |
| | | var goodTypes = [MarketTypeModel]() |
| | | var orderNumber: String = "" |
| | | var residueNumber:Int? |
| | | var recipient: MarketRecipientModel? |
| | | } |
| | | |
| | | struct MarketRecipientModel:HandyJSON,Hashable{ |
| | | var id = 0 |
| | | var name = "" |
| | | var id = 0 |
| | | var name = "" |
| | | } |
| | | |
| | | struct AddressModel:HandyJSON{ |
| | | var address: String = "" |
| | | var city: String = "" |
| | | var cityCode: String = "" |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var disabled: Bool = false |
| | | var id: Int = 0 |
| | | var isDefault: Int = 0 |
| | | var province: String = "" |
| | | var provinceCode: String = "" |
| | | var recipient: String = "" |
| | | var recipientPhone: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var userId: Int = 0 |
| | | var orderId:Int = 0 |
| | | var address: String = "" |
| | | var city: String = "" |
| | | var cityCode: String = "" |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var disabled: Bool = false |
| | | var id: Int = 0 |
| | | var isDefault: Int = 0 |
| | | var province: String = "" |
| | | var provinceCode: String = "" |
| | | var recipient: String = "" |
| | | var recipientPhone: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var userId: Int = 0 |
| | | var orderId:Int = 0 |
| | | } |
| | | |
| | | struct AddressTreeModel:HandyJSON{ |
| | | var id = 0 |
| | | var name = "" |
| | | var code = "" |
| | | var parentId = 0 |
| | | var children:[AddressTreeModel]? |
| | | var id = 0 |
| | | var name = "" |
| | | var code = "" |
| | | var parentId = 0 |
| | | var children:[AddressTreeModel]? |
| | | } |
| | | |
| | | struct IntegralModel:HandyJSON{ |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var disabled: Bool = false |
| | | var gameId: Int = 0 |
| | | var id: Int = 0 |
| | | var integral: String = "" |
| | | var method: String = "" |
| | | var storyId: Int = 0 |
| | | var type: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var userId: Int = 0 |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var disabled: Bool = false |
| | | var gameId: Int = 0 |
| | | var id: Int = 0 |
| | | var integral: String = "" |
| | | var method: String = "" |
| | | var storyId: Int = 0 |
| | | var type: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var userId: Int = 0 |
| | | } |
| | | |
| | | struct ExchangeRecordModel:HandyJSON{ |
| | | var completeTime: String = "" |
| | | var consigneeAddress: String = "" |
| | | var consigneeName: String = "" |
| | | var consigneePhone: String = "" |
| | | var count: Int = 0 |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var disabled: Bool = false |
| | | var express: String = "" |
| | | var expressNumber: String = "" |
| | | var expressTime: String = "" |
| | | var goodsId: Int = 0 |
| | | var goodsName: String = "" |
| | | var id: Int = 0 |
| | | var insertTime: String = "" |
| | | var integral: Int = 0 |
| | | var orderNumber: String = "" |
| | | var orderId:Int = 0 |
| | | var state: Int = 0 //订单状态1待发货2已发货3已完成 |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var userId: Int = 0 |
| | | var goodsType = [String]() |
| | | var coverImg:String = "" |
| | | var completeTime: String = "" |
| | | var consigneeAddress: String = "" |
| | | var consigneeName: String = "" |
| | | var consigneePhone: String = "" |
| | | var count: Int = 0 |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var disabled: Bool = false |
| | | var express: String = "" |
| | | var expressNumber: String = "" |
| | | var expressTime: String = "" |
| | | var goodsId: Int = 0 |
| | | var goodsName: String = "" |
| | | var id: Int = 0 |
| | | var insertTime: String = "" |
| | | var integral: Int = 0 |
| | | var orderNumber: String = "" |
| | | var orderId:Int = 0 |
| | | var state: Int = 0 //订单状态1待发货2已发货3已完成 |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var userId: Int = 0 |
| | | var goodsType = [String]() |
| | | var coverImg:String = "" |
| | | } |
| | | |
| | | struct StudyGamesModel:HandyJSON{ |
| | | var gameRecordList = [StudyGamesRecordModel]() |
| | | var record:StudyDataRecordModel? |
| | | var gameRecordList = [StudyGamesRecordModel]() |
| | | var record:StudyDataRecordModel? |
| | | } |
| | | |
| | | |
| | | struct StudyGamesRecordModel:HandyJSON{ |
| | | var accuracy: Int = 0 |
| | | var createBy: String = "" |
| | | var time = "" |
| | | // var createTime: String = "" |
| | | var disabled: Bool = false |
| | | var gameDifficulty: Int = 0 |
| | | var gameId: Int = 0 |
| | | var gameName: String = "" |
| | | var id: Int = 0 |
| | | var updateBy: String = "" |
| | | // var updateTime: String = "" |
| | | var userId: Int = 0 |
| | | var useTime: Int = 0 |
| | | var accuracy: Int = 0 |
| | | var createBy: String = "" |
| | | var time = "" |
| | | // var createTime: String = "" |
| | | var disabled: Bool = false |
| | | var gameDifficulty: Int = 0 |
| | | var gameId: Int = 0 |
| | | var gameName: String = "" |
| | | var id: Int = 0 |
| | | var updateBy: String = "" |
| | | // var updateTime: String = "" |
| | | var userId: Int = 0 |
| | | var useTime: Int = 0 |
| | | } |
| | | |
| | | struct StudyDataRecordModel:HandyJSON{ |
| | | var answer: Int = 0 |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var day: Int = 0 |
| | | var disabled: Bool = false |
| | | var id: Int = 0 |
| | | var induction: Int = 0 |
| | | var listen: Int = 0 |
| | | var look: Int = 0 |
| | | var monthStudy: Int = 0 |
| | | var pair: Int = 0 |
| | | var surplus: Int = 0 |
| | | var todayStudy: Int = 0 |
| | | var totalStudy: Int = 0 |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var userId: Int = 0 |
| | | var week: Int = 0 |
| | | var weekStudy: Int = 0 |
| | | var answer: Int = 0 |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var day: Int = 0 |
| | | var disabled: Bool = false |
| | | var id: Int = 0 |
| | | var induction: Int = 0 |
| | | var listen: Int = 0 |
| | | var look: Int = 0 |
| | | var monthStudy: Int = 0 |
| | | var pair: Int = 0 |
| | | var surplus: Int = 0 |
| | | var todayStudy: Int = 0 |
| | | var totalStudy: Int = 0 |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var userId: Int = 0 |
| | | var week: Int = 0 |
| | | var weekStudy: Int = 0 |
| | | } |
| | | |
| | | struct ListenWeekModel:HandyJSON{ |
| | | var id = 0 |
| | | var day = 0 |
| | | var quarter = 0 |
| | | var title = "" |
| | | var totalIntegral = 0 |
| | | var type = 0 |
| | | var week = 0 |
| | | var canStudy = 0 |
| | | var id = 0 |
| | | var day = 0 |
| | | var quarter = 0 |
| | | var title = "" |
| | | var totalIntegral = 0 |
| | | var type = 0 |
| | | var week = 0 |
| | | var canStudy = 0 |
| | | } |
| | | |
| | | class ListenNewModel:HandyJSON{ |
| | | var data:ListenNewDataModel? |
| | | var subjectList = [[Listen1SubModel]]() |
| | | var data:ListenNewDataModel? |
| | | var list = [ListenSubCardModel]() |
| | | var subjectList = [[Listen1SubModel]]() |
| | | var accuracy:Double = 0 |
| | | |
| | | required init(){} |
| | | required init(){} |
| | | } |
| | | |
| | | class ListenNewDataModel:HandyJSON{ |
| | | var id:String = "" |
| | | var integral = 0 |
| | | var id:String = "" |
| | | var integral = 0 |
| | | |
| | | required init(){} |
| | | required init(){} |
| | | } |
| | | |
| | | class Listen1Model:HandyJSON{ |
| | | var data:Listen1DataModel? |
| | | var subjectList = [Listen1SubModel]() |
| | | var storyList = [Listen1SubModel]() |
| | | var data:Listen1DataModel? |
| | | var subjectList = [Listen1SubModel]() |
| | | var storyList = [Listen1SubModel]() |
| | | |
| | | //超级记忆专用 |
| | | var photoList = [SimpleListenDataModel]() |
| | | var voiceList = [SimpleListenDataModel]() |
| | | //超级记忆专用 |
| | | var photoList = [SimpleListenDataModel]() |
| | | var voiceList = [SimpleListenDataModel]() |
| | | |
| | | required init(){} |
| | | required init(){} |
| | | } |
| | | |
| | | struct TeamScheduleModel:HandyJSON{ |
| | | var answerNumber = 0 |
| | | var correctNumber = 0 |
| | | var teamIds = [Int]() //题组ids |
| | | var topicIds = [Int]() //已回答正确的题目Id |
| | | var schedule = 0 |
| | | var answerNumber = 0 |
| | | var correctNumber = 0 |
| | | var teamIds = [Int]() //题组ids |
| | | var topicIds = [Int]() //已回答正确的题目Id |
| | | var schedule = 0 |
| | | } |
| | | |
| | | |
| | | struct Listen1DataModel:HandyJSON{ |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var day: Int = 0 |
| | | var disabled: Bool = false |
| | | var id: Int = 0 |
| | | var integral: Int = 0 |
| | | var isVip: Int = 0 |
| | | var studyId: Int = 0 |
| | | var subject: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var week: Int = 0 |
| | | var answerCount = 0 |
| | | var answerIntegral = 0 |
| | | var answerTime = 0 |
| | | var time = 0 |
| | | var count = 0 |
| | | var lookIntegral = 0 |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var day: Int = 0 |
| | | var disabled: Bool = false |
| | | var id: Int = 0 |
| | | var integral: Int = 0 |
| | | var isVip: Int = 0 |
| | | var studyId: Int = 0 |
| | | var subject: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var week: Int = 0 |
| | | var answerCount = 0 |
| | | var answerIntegral = 0 |
| | | var answerTime = 0 |
| | | var time = 0 |
| | | var count = 0 |
| | | var lookIntegral = 0 |
| | | |
| | | //custom |
| | | var playNow:Bool = false //立刻播放 |
| | | //custom |
| | | var playNow:Bool = false //立刻播放 |
| | | |
| | | } |
| | | |
| | | class ListenSubCardModel:HandyJSON{ |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var day: Int = 0 |
| | | var disabled: Bool = false |
| | | var id: Int = 0 |
| | | var objectId: Int = 0 |
| | | var one: Int = 0 |
| | | var status: Int = 0 |
| | | var two: Int = 0 |
| | | var type: Int = 0 |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var userId: Int = 0 |
| | | var week: Int = 0 |
| | | |
| | | required init(){} |
| | | } |
| | | |
| | | class Listen1SubModel:HandyJSON,Hashable{ |
| | | |
| | | static func == (lhs: Listen1SubModel, rhs: Listen1SubModel) -> Bool { |
| | | return lhs.id == rhs.id |
| | | } |
| | | |
| | | // var hashValue: Int{ |
| | | // return id |
| | | // } |
| | | static func == (lhs: Listen1SubModel, rhs: Listen1SubModel) -> Bool { |
| | | return lhs.id == rhs.id |
| | | } |
| | | |
| | | func hash(into hasher: inout Hasher) { |
| | | // var hashValue: Int{ |
| | | // return id |
| | | // } |
| | | |
| | | } |
| | | func hash(into hasher: inout Hasher) { |
| | | |
| | | } |
| | | |
| | | |
| | | required init() {} |
| | | required init() {} |
| | | |
| | | var correct: String = "" |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var disabled: Bool = false |
| | | var english: String = "" |
| | | var error: String = "" |
| | | var id: Int = 0 |
| | | var img: String = "" |
| | | var name: String = "" |
| | | var state: Int = 0 |
| | | var type: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var correct: String = "" |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var disabled: Bool = false |
| | | var english: String = "" |
| | | var error: String = "" |
| | | var id: Int = 0 |
| | | var img: String = "" |
| | | var name: String = "" |
| | | var state: Int = 0 |
| | | var type: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | |
| | | |
| | | //学习类型四专用 |
| | | var isQuestion:Int = 0 |
| | | //学习类型四专用 |
| | | var isQuestion:Int = 0 |
| | | |
| | | //游戏类型2专用 |
| | | var isOpen:Bool = false |
| | | //游戏类型2专用 |
| | | var isOpen:Bool = false |
| | | |
| | | // 自主学习1,3专用 (是否已回答) |
| | | var isAnster:Bool = false |
| | | // 自主学习1,3专用 (是否已回答) |
| | | var isAnster:Bool = false |
| | | } |
| | | |
| | | @available(*,deprecated,message: "废弃") |
| | | struct Listen4Model:HandyJSON{ |
| | | var data = [Listen4DataModel]() |
| | | var data = [Listen4DataModel]() |
| | | } |
| | | |
| | | struct Listen4DataModel:HandyJSON{ |
| | | var answerSubject: Int = 0 |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var day: Int = 0 |
| | | var disabled: Bool = false |
| | | var id: Int = 0 |
| | | var integral: Int = 0 |
| | | var isAnswer: Int = 0 |
| | | var isVip: Int = 0 |
| | | var studyId: Int = 0 |
| | | var subject: Int = 0 |
| | | var subjectList = [Listen1SubModel]() |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var week: Int = 0 |
| | | var answerSubject: Int = 0 |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var day: Int = 0 |
| | | var disabled: Bool = false |
| | | var id: Int = 0 |
| | | var integral: Int = 0 |
| | | var isAnswer: Int = 0 |
| | | var isVip: Int = 0 |
| | | var studyId: Int = 0 |
| | | var subject: Int = 0 |
| | | var subjectList = [Listen1SubModel]() |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var week: Int = 0 |
| | | } |
| | | |
| | | struct StudyScheduleModel:HandyJSON,Hashable{ |
| | | init() {} |
| | | |
| | | var answer: Int = 0 |
| | | var day: Int = 0 |
| | | var computeSchedule:Int = 0 |
| | | var id: Int = 0 |
| | | var induction: Int = 0 |
| | | var listen: Int = 0 |
| | | var look: Int = 0 |
| | | var monthStudy: Int = 0 |
| | | var pair: Int = 0 |
| | | var surplus: String = "" |
| | | var todayStudy: Int = 0 |
| | | var totalStudy: Int = 0 |
| | | var userId: Int = 0 |
| | | var week: Int = 0 |
| | | var weekStudy: Int = 0 |
| | | var gameDifficulty:Int = 0 |
| | | init() {} |
| | | |
| | | var hashValue: Int{ |
| | | return answer+day+computeSchedule+induction+listen+look+monthStudy+pair+todayStudy+totalStudy+week+weekStudy+gameDifficulty |
| | | } |
| | | var answer: Int = 0 |
| | | var day: Int = 0 |
| | | var computeSchedule:Int = 0 |
| | | var id: Int = 0 |
| | | var induction: Int = 0 |
| | | var listen: Int = 0 |
| | | var look: Int = 0 |
| | | var monthStudy: Int = 0 |
| | | var pair: Int = 0 |
| | | var surplus: String = "" |
| | | var todayStudy: Int = 0 |
| | | var totalStudy: Int = 0 |
| | | var userId: Int = 0 |
| | | var week: Int = 0 |
| | | var weekStudy: Int = 0 |
| | | var gameDifficulty:Int = 0 |
| | | |
| | | var hashValue: Int{ |
| | | return answer+day+computeSchedule+induction+listen+look+monthStudy+pair+todayStudy+totalStudy+week+weekStudy+gameDifficulty |
| | | } |
| | | } |
| | | |
| | | class SimpleListenDataModel:HandyJSON,Hashable{ |
| | | |
| | | required init() {} |
| | | required init() {} |
| | | |
| | | static func == (lhs: SimpleListenDataModel, rhs: SimpleListenDataModel) -> Bool { |
| | | return lhs.id == rhs.id |
| | | } |
| | | static func == (lhs: SimpleListenDataModel, rhs: SimpleListenDataModel) -> Bool { |
| | | return lhs.id == rhs.id |
| | | } |
| | | |
| | | func hash(into hasher: inout Hasher) { |
| | | func hash(into hasher: inout Hasher) { |
| | | |
| | | } |
| | | } |
| | | |
| | | var id = 0 |
| | | var photo = "" |
| | | var voice = "" |
| | | var id = 0 |
| | | var photo = "" |
| | | var voice = "" |
| | | |
| | | //游戏类型2专用 |
| | | var isOpen:Bool = false |
| | | var type = 0 // 1:图片 2:音频 |
| | | //游戏类型2专用 |
| | | var isOpen:Bool = false |
| | | var type = 0 // 1:图片 2:音频 |
| | | } |
| | | |
| | | struct PromptVoiceModel:HandyJSON{ |
| | | var correct: String = "" |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var disabled: Bool = false |
| | | var error: String = "" |
| | | var id: Int = 0 |
| | | var img: String = "" |
| | | var integral: String = "" |
| | | var integralShare: String = "" |
| | | var phone: String = "" |
| | | var time: String = "" |
| | | var title: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | var correct: String = "" |
| | | var createBy: String = "" |
| | | var createTime: String = "" |
| | | var disabled: Bool = false |
| | | var error: String = "" |
| | | var id: Int = 0 |
| | | var img: String = "" |
| | | var integral: String = "" |
| | | var integralShare: String = "" |
| | | var phone: String = "" |
| | | var time: String = "" |
| | | var title: String = "" |
| | | var updateBy: String = "" |
| | | var updateTime: String = "" |
| | | } |
| | | |
| | | struct ShareInfoModel:HandyJSON{ |
| | | var title = "" |
| | | var phone = "" |
| | | var img = "" |
| | | var title = "" |
| | | var phone = "" |
| | | var img = "" |
| | | } |
| | | |
| | | struct VIPInfoModel:HandyJSON{ |
| | | var id = 0 |
| | | var info = "" |
| | | var isVip = 0 |
| | | var time = 0 |
| | | var amount = 0 |
| | | var id = 0 |
| | | var info = "" |
| | | var isVip = 0 |
| | | var time = 0 |
| | | var amount = 0 |
| | | } |
| | | |
| | | struct PaymentInfoModel:HandyJSON{ |
| | | var id = 0 |
| | | var orderId = 0 |
| | | var id = 0 |
| | | var orderId = 0 |
| | | } |
| | |
| | | import RxRelay |
| | | |
| | | class FightAnswerViewModel{ |
| | | var selectIndex = BehaviorRelay<IndexPath?>(value: nil) |
| | | var answerType = BehaviorRelay<Fight_lessonType>(value: .none) |
| | | var selectIndex = BehaviorRelay<IndexPath?>(value: nil) |
| | | var answerType = BehaviorRelay<Fight_lessonType>(value: .none) |
| | | } |
| | | |
| | | /// 题目类型一 |
| | | class HomeListenFight_lesson_1_VC: BaseVC { |
| | | |
| | | private var viewModel = FightAnswerViewModel() |
| | | private var listenNewModel:ListenNewModel! |
| | | private var randomElement:Listen1SubModel? |
| | | private var page:Int! |
| | | private(set) var isListen:Bool = false |
| | | private var isAnsterComplete:Bool = false //是否已经回答完成[小题] |
| | | private var isAnsterDone:Bool = false //是否已经回答完成[大题] |
| | | private var viewModel = FightAnswerViewModel() |
| | | private var listenNewModel:ListenNewModel! |
| | | private var randomElement:Listen1SubModel? |
| | | private var page:Int! |
| | | private(set) var isListen:Bool = false |
| | | private var isAnsterComplete:Bool = false //是否已经回答完成[小题] |
| | | private var isAnsterDone:Bool = false //是否已经回答完成[大题] |
| | | |
| | | private var isAnsterModel = [Listen1SubModel]() |
| | | private var menuView:VoiceHandleView? |
| | | private var isAnsterModel = [Listen1SubModel]() |
| | | |
| | | private lazy var collectionView:UICollectionView = { |
| | | let flowLayout = UICollectionViewFlowLayout() |
| | | let w = (JQ_ScreenW - 194 * 2 - 25) / 2 |
| | | flowLayout.itemSize = CGSize(width: w, height: w * 0.745) |
| | | flowLayout.minimumLineSpacing = 25 |
| | | flowLayout.minimumInteritemSpacing = 25 |
| | | flowLayout.scrollDirection = .vertical |
| | | let collection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) |
| | | collection.register(UINib(nibName: "ListenFight_lesson_1_CCell", bundle: nil), forCellWithReuseIdentifier: "_ListenFight_lesson_1_CCell") |
| | | collection.isScrollEnabled = false |
| | | return collection |
| | | }() |
| | | //已回答过的题 |
| | | private var answerList = [Listen1SubModel]() |
| | | |
| | | var rootViewModel:HomeListenFightViewModel! |
| | | var teamScheduleModel:TeamScheduleModel?{ |
| | | didSet{ |
| | | if let m = teamScheduleModel{ |
| | | var temp = [Listen1SubModel]() |
| | | for v in listenNewModel.subjectList[page]{ |
| | | //已回答 |
| | | if m.topicIds.contains(v.id){ |
| | | temp.append(v) |
| | | } |
| | | } |
| | | isAnsterModel.insert(contentsOf: temp, at: 0) |
| | | private var menuView:VoiceHandleView? |
| | | private var handleClouse:(()->Void)? |
| | | |
| | | //todo |
| | | // let teamId = weakSelf.listenNewModel.data?.id.components(separatedBy: ",")[weakSelf.page] |
| | | // weakSelf.rootViewModel.insertCorrectAnswer(teamId: teamId, answerId: weakSelf.listenNewModel.subjectList[weakSelf.page][index.row].id) |
| | | private lazy var collectionView:UICollectionView = { |
| | | let flowLayout = UICollectionViewFlowLayout() |
| | | let w = (JQ_ScreenW - 194 * 2 - 25) / 2 |
| | | flowLayout.itemSize = CGSize(width: w, height: w * 0.745) |
| | | flowLayout.minimumLineSpacing = 25 |
| | | flowLayout.minimumInteritemSpacing = 25 |
| | | flowLayout.scrollDirection = .vertical |
| | | let collection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) |
| | | collection.register(UINib(nibName: "ListenFight_lesson_1_CCell", bundle: nil), forCellWithReuseIdentifier: "_ListenFight_lesson_1_CCell") |
| | | collection.isScrollEnabled = false |
| | | return collection |
| | | }() |
| | | |
| | | var rootViewModel:HomeListenFightViewModel! |
| | | var teamScheduleModel:TeamScheduleModel?{ |
| | | didSet{ |
| | | if let m = teamScheduleModel{ |
| | | var temp = [Listen1SubModel]() |
| | | for v in listenNewModel.subjectList[page]{ |
| | | //已回答 |
| | | if m.topicIds.contains(v.id){ |
| | | temp.append(v) |
| | | } |
| | | } |
| | | isAnsterModel.insert(contentsOf: temp, at: 0) |
| | | } |
| | | } |
| | | } |
| | | |
| | | required init(page:Int,listenNewModel:ListenNewModel){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.page = page |
| | | self.listenNewModel = listenNewModel |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | navigationItem.titleView = UIView() |
| | | collectionView.reloadData() |
| | | print("加载======DidLoad") |
| | | |
| | | //回传记录,始终保持答题进度 |
| | | // if let team = teamScheduleModel{ |
| | | // let v = team.schedule - 1 |
| | | // for i in 0..<v{ |
| | | // isAnsterModel.append(listenNewModel.subjectList[page][i]) |
| | | // } |
| | | // } |
| | | |
| | | //制造随机 |
| | | // listenNewModel.subjectList[page].shuffle() |
| | | getNextAnswer(isFirst: true) |
| | | menuView?.listenType = .lesson1 |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | VoicePlayer.share().playerAt(url: self.randomElement?.correct) |
| | | self.menuView?.playing() |
| | | } |
| | | } |
| | | |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | super.viewDidAppear(animated) |
| | | VoicePlayer.share().delegate = self |
| | | } |
| | | |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | VoicePlayer.share().delegate = nil |
| | | VoicePlayer.share().playerInterrupt() |
| | | } |
| | | |
| | | |
| | | func restore(){ |
| | | teamScheduleModel?.topicIds.removeAll() |
| | | teamScheduleModel?.teamIds.removeAll() |
| | | answerList.removeAll() |
| | | rootViewModel.currentPage.accept(0) |
| | | rootViewModel.answerCount.accept(1) |
| | | isAnsterDone = false |
| | | isAnsterModel.removeAll() |
| | | isAnsterComplete = false |
| | | viewModel.answerType.accept(.none) |
| | | viewModel.selectIndex.accept(nil) |
| | | menuView?.resetView() |
| | | setUI() |
| | | collectionView.reloadData() |
| | | getNextAnswer(isFirst: true) |
| | | } |
| | | |
| | | } |
| | | } |
| | | } |
| | | func tobefore(){ |
| | | if isAnsterModel.count == 1{return} |
| | | |
| | | required init(page:Int,listenNewModel:ListenNewModel){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.page = page |
| | | self.listenNewModel = listenNewModel |
| | | } |
| | | isAnsterModel.removeLast() |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | rootViewModel.answerCount.accept(page + isAnsterModel.count) |
| | | |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | navigationItem.titleView = UIView() |
| | | collectionView.reloadData() |
| | | print("加载======DidLoad") |
| | | randomElement = isAnsterModel.last |
| | | menuView?.playUrl = randomElement?.correct |
| | | VoicePlayer.share().playerAt(url: self.randomElement?.correct) |
| | | menuView?.playing() |
| | | |
| | | //回传记录,始终保持答题进度 |
| | | if let team = teamScheduleModel{ |
| | | let v = team.schedule - 1 |
| | | for i in 0..<v{ |
| | | isAnsterModel.append(listenNewModel.subjectList[page][i]) |
| | | } |
| | | } |
| | | // listenNewModel.subjectList[page].shuffle() |
| | | collectionView.reloadData() |
| | | } |
| | | |
| | | //制造随机 |
| | | listenNewModel.subjectList[page].shuffle() |
| | | getNextAnswer(isFirst: true) |
| | | menuView?.listenType = .lesson1 |
| | | /// 下一题 |
| | | /// - Parameter isFirst: 是否首次进入,首次页码不+1 |
| | | private func getNextAnswer(isFirst:Bool = false){ |
| | | isListen = false |
| | | if isAnsterModel.count == 4{ |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | VoicePlayer.share().playerAt(url: self.randomElement?.correct) |
| | | self.menuView?.playing() |
| | | } |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2){[weak self] in |
| | | guard let weakSelf = self else { return } |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: nil) |
| | | weakSelf.viewModel.answerType.accept(.none) |
| | | weakSelf.viewModel.selectIndex.accept(nil) |
| | | weakSelf.isListen = false |
| | | let v = weakSelf.rootViewModel.answerCount.value |
| | | weakSelf.rootViewModel.answerCount.accept(v + 1) |
| | | } |
| | | return |
| | | } |
| | | |
| | | randomElement = listenNewModel.subjectList[page].randomElement() //随机抽题 |
| | | |
| | | if randomElement != nil{ |
| | | //如果已经回答,或标记为已回答(返回上一小题时) |
| | | if isAnsterModel.contains(randomElement!){ |
| | | answerList.append(randomElement!) |
| | | getNextAnswer();return |
| | | } |
| | | |
| | | //没有回答 |
| | | if !isAnsterModel.contains(randomElement!){ |
| | | isAnsterModel.append(randomElement!) |
| | | } |
| | | |
| | | } |
| | | menuView?.playUrl = randomElement?.correct |
| | | |
| | | // listenNewModel.subjectList[page].shuffle() |
| | | isAnsterComplete = false |
| | | viewModel.answerType.accept(.none) |
| | | setUI() |
| | | |
| | | //自动播放下一题语音 |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | VoicePlayer.share().playerAt(url: self.randomElement?.correct) |
| | | self.menuView?.playing() |
| | | } |
| | | |
| | | if !isAnsterDone && isAnsterModel.count <= 4 && !isFirst{ |
| | | let v = rootViewModel.answerCount.value |
| | | rootViewModel.answerCount.accept(v + 1) |
| | | } |
| | | } |
| | | |
| | | override func setUI() { |
| | | super.setUI() |
| | | |
| | | if !view.subviews.contains(collectionView){ |
| | | collectionView.delegate = self |
| | | collectionView.dataSource = self |
| | | collectionView.backgroundColor = UIColor(hexStr: "#C3BFB3") |
| | | view.addSubview(collectionView) |
| | | collectionView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(101) |
| | | make.left.equalTo(194) |
| | | make.right.equalTo(-194) |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | } |
| | | |
| | | if menuView == nil{ |
| | | menuView = VoiceHandleView() |
| | | view.addSubview(menuView!) |
| | | menuView?.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(24) |
| | | make.centerX.equalToSuperview() |
| | | // make.height.equalTo(52) |
| | | make.height.equalTo(40) |
| | | // make.width.equalTo(159) |
| | | make.width.equalTo(189) |
| | | } |
| | | }else{ |
| | | menuView?.snp.remakeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(24) |
| | | make.centerX.equalToSuperview() |
| | | // make.height.equalTo(52) |
| | | make.height.equalTo(40) |
| | | // make.width.equalTo(159) |
| | | make.width.equalTo(189) |
| | | } |
| | | } |
| | | |
| | | collectionView.reloadData() |
| | | } |
| | | |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | | let flowLayout = self.collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | let w = (self.collectionView.size.width - flowLayout.minimumLineSpacing) / 2 |
| | | let h = (self.collectionView.size.height - flowLayout.minimumInteritemSpacing) / 2 |
| | | |
| | | if flowLayout.itemSize.width != w || flowLayout.itemSize.height != h{ |
| | | flowLayout.itemSize = CGSize(width: w, height: h) |
| | | collectionView.reloadData() |
| | | } |
| | | } |
| | | |
| | | override func setRx() { |
| | | viewModel.selectIndex.subscribe(onNext: {[weak self] index in |
| | | guard let weakSelf = self else { return } |
| | | guard let index = index else { return } |
| | | if weakSelf.viewModel.answerType.value == .success {return} |
| | | //判断选中是否正确逻辑 |
| | | if let cell = self?.collectionView.cellForItem(at: index) as? ListenFight_lesson_1_CCell{ |
| | | var answer:Fight_lessonType = .none |
| | | if self?.randomElement?.id == weakSelf.listenNewModel.subjectList[weakSelf.page][index.row].id{ |
| | | answer = .success |
| | | self?.randomElement?.isAnster = true |
| | | self?.isListen = false |
| | | if self?.isAnsterComplete == false{ |
| | | self?.rootViewModel.correctNum += 1 |
| | | } |
| | | self?.isAnsterComplete = true |
| | | |
| | | VoicePlayer.share().playSuccessVoice() |
| | | |
| | | let teamId = weakSelf.listenNewModel.data?.id.components(separatedBy: ",")[weakSelf.page] |
| | | weakSelf.rootViewModel.insertCorrectAnswer(teamId: teamId, answerId: weakSelf.listenNewModel.subjectList[weakSelf.page][index.row].id) |
| | | |
| | | var cin = 0 |
| | | for (ci,v) in weakSelf.listenNewModel.subjectList[weakSelf.page].enumerated(){ |
| | | if v.id == self?.randomElement?.id{ |
| | | cin = ci;break |
| | | } |
| | | } |
| | | |
| | | if weakSelf.page >= 1{cin += 4} |
| | | |
| | | let model = weakSelf.listenNewModel.list[cin] |
| | | model.status = 2 |
| | | Services.answerQuestion(id: model.id, status: 2).subscribe(onNext: {_ in |
| | | weakSelf.handleClouse?() |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | VoicePlayer.share().playerAt(url: weakSelf.listenNewModel.subjectList[weakSelf.page][index.row].correct) |
| | | } |
| | | }else{ |
| | | answer = .fail |
| | | VoicePlayer.share().playFailVoice() |
| | | self?.isListen = false |
| | | if self?.isAnsterComplete == false{ |
| | | self?.rootViewModel.errorNum += 1 |
| | | } |
| | | |
| | | var cin = 0 |
| | | for (ci,v) in weakSelf.listenNewModel.subjectList[weakSelf.page].enumerated(){ |
| | | if v.id == self?.randomElement?.id{ |
| | | cin = ci;break |
| | | } |
| | | } |
| | | |
| | | if weakSelf.page >= 1{cin += 4} |
| | | let model = weakSelf.listenNewModel.list[cin] |
| | | model.status = 3 |
| | | Services.answerQuestion(id: model.id, status: 3).subscribe(onNext: {_ in |
| | | weakSelf.handleClouse?() |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | } |
| | | |
| | | |
| | | switch answer { |
| | | case .success: |
| | | self?.viewModel.answerType.accept(.success) |
| | | self?.answerSuccess(cell) |
| | | case .fail: |
| | | self?.viewModel.answerType.accept(.fail) |
| | | self?.collectionView.reloadData() |
| | | default:break |
| | | } |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | |
| | | NotificationCenter.default.rx.notification(ResetLession_Noti).take(until: self.rx.deallocated).subscribe(onNext: {[weak self]_ in |
| | | self?.restore() |
| | | self?.viewDidLoad() |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | } |
| | | //回答正确 |
| | | private func answerSuccess(_ cell:ListenFight_lesson_1_CCell){ |
| | | menuView?.snp.removeConstraints() |
| | | menuView?.playing() |
| | | menuView?.jq_cornerRadius = 0 |
| | | let v = cell.view_topHandle.convert(cell.bounds, to: self.view) |
| | | UIView.animate(withDuration: 0.3) { |
| | | self.menuView?.snp.updateConstraints { make in |
| | | make.top.equalTo(self.view).offset(v.origin.y) |
| | | make.left.equalToSuperview().offset(v.origin.x) |
| | | make.width.equalTo(v.size.width - 10) |
| | | make.height.equalTo(40) |
| | | } |
| | | self.view.layoutIfNeeded() |
| | | }completion: { _ in |
| | | self.collectionView.reloadData() |
| | | } |
| | | } |
| | | |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | super.viewDidAppear(animated) |
| | | VoicePlayer.share().delegate = self |
| | | } |
| | | |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | VoicePlayer.share().delegate = nil |
| | | VoicePlayer.share().playerInterrupt() |
| | | } |
| | | |
| | | |
| | | func restore(){ |
| | | isAnsterDone = false |
| | | isAnsterModel.removeAll() |
| | | isAnsterComplete = false |
| | | viewModel.answerType.accept(.none) |
| | | viewModel.selectIndex.accept(nil) |
| | | menuView?.resetView() |
| | | setUI() |
| | | collectionView.reloadData() |
| | | getNextAnswer(isFirst: true) |
| | | } |
| | | |
| | | func tobefore(){ |
| | | if isAnsterModel.count == 1{return} |
| | | |
| | | isAnsterModel.removeLast() |
| | | |
| | | rootViewModel.answerCount.accept(page + isAnsterModel.count) |
| | | |
| | | randomElement = isAnsterModel.last |
| | | menuView?.playUrl = randomElement?.correct |
| | | VoicePlayer.share().playerAt(url: self.randomElement?.correct) |
| | | menuView?.playing() |
| | | |
| | | listenNewModel.subjectList[page].shuffle() |
| | | collectionView.reloadData() |
| | | } |
| | | |
| | | /// 下一题 |
| | | /// - Parameter isFirst: 是否首次进入,首次页码不+1 |
| | | private func getNextAnswer(isFirst:Bool = false){ |
| | | isListen = false |
| | | if isAnsterModel.count == 4{ |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+3){[weak self] in |
| | | guard let weakSelf = self else { return } |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: nil) |
| | | weakSelf.viewModel.answerType.accept(.none) |
| | | weakSelf.viewModel.selectIndex.accept(nil) |
| | | weakSelf.isListen = false |
| | | let v = weakSelf.rootViewModel.answerCount.value |
| | | weakSelf.rootViewModel.answerCount.accept(v + 1) |
| | | } |
| | | return |
| | | } |
| | | |
| | | randomElement = listenNewModel.subjectList[page].randomElement() //随机抽题 |
| | | if randomElement != nil{ |
| | | //如果已经回答,或标记为已回答(返回上一小题时) |
| | | if isAnsterModel.contains(randomElement!){ |
| | | getNextAnswer();return |
| | | } |
| | | |
| | | //没有回答 |
| | | if !isAnsterModel.contains(randomElement!){ |
| | | isAnsterModel.append(randomElement!) |
| | | } |
| | | |
| | | } |
| | | menuView?.playUrl = randomElement?.correct |
| | | |
| | | listenNewModel.subjectList[page].shuffle() |
| | | isAnsterComplete = false |
| | | viewModel.answerType.accept(.none) |
| | | collectionView.reloadData() |
| | | setUI() |
| | | |
| | | //自动播放下一题语音 |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | VoicePlayer.share().playerAt(url: self.randomElement?.correct) |
| | | self.menuView?.playing() |
| | | } |
| | | |
| | | if !isAnsterDone && isAnsterModel.count <= 4 && !isFirst{ |
| | | let v = rootViewModel.answerCount.value |
| | | rootViewModel.answerCount.accept(v + 1) |
| | | } |
| | | } |
| | | |
| | | override func setUI() { |
| | | super.setUI() |
| | | |
| | | if !view.subviews.contains(collectionView){ |
| | | collectionView.delegate = self |
| | | collectionView.dataSource = self |
| | | collectionView.backgroundColor = UIColor(hexStr: "#C3BFB3") |
| | | view.addSubview(collectionView) |
| | | collectionView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(101) |
| | | make.left.equalTo(194) |
| | | make.right.equalTo(-194) |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | } |
| | | |
| | | if menuView == nil{ |
| | | menuView = VoiceHandleView() |
| | | view.addSubview(menuView!) |
| | | menuView?.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(24) |
| | | make.centerX.equalToSuperview() |
| | | make.height.equalTo(52) |
| | | make.width.equalTo(159) |
| | | } |
| | | }else{ |
| | | menuView?.snp.remakeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(24) |
| | | make.centerX.equalToSuperview() |
| | | make.height.equalTo(52) |
| | | make.width.equalTo(159) |
| | | } |
| | | } |
| | | |
| | | collectionView.reloadData() |
| | | } |
| | | |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | | let flowLayout = self.collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | let w = (self.collectionView.size.width - flowLayout.minimumLineSpacing) / 2 |
| | | let h = (self.collectionView.size.height - flowLayout.minimumInteritemSpacing) / 2 |
| | | |
| | | if flowLayout.itemSize.width != w || flowLayout.itemSize.height != h{ |
| | | flowLayout.itemSize = CGSize(width: w, height: h) |
| | | collectionView.reloadData() |
| | | } |
| | | } |
| | | |
| | | override func setRx() { |
| | | viewModel.selectIndex.subscribe(onNext: {[weak self] index in |
| | | guard let weakSelf = self else { return } |
| | | guard let index = index else { return } |
| | | if weakSelf.viewModel.answerType.value == .success {return} |
| | | //判断选中是否正确逻辑 |
| | | if let cell = self?.collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_1_CCell", for: index) as? ListenFight_lesson_1_CCell{ |
| | | var answer:Fight_lessonType = .none |
| | | if self?.randomElement?.id == weakSelf.listenNewModel.subjectList[weakSelf.page][index.row].id{ |
| | | answer = .success |
| | | self?.randomElement?.isAnster = true |
| | | self?.isListen = false |
| | | if self?.isAnsterComplete == false{ |
| | | self?.rootViewModel.correctNum += 1 |
| | | } |
| | | self?.isAnsterComplete = true |
| | | |
| | | VoicePlayer.share().playSuccessVoice() |
| | | |
| | | let teamId = weakSelf.listenNewModel.data?.id.components(separatedBy: ",")[weakSelf.page] |
| | | weakSelf.rootViewModel.insertCorrectAnswer(teamId: teamId, answerId: weakSelf.listenNewModel.subjectList[weakSelf.page][index.row].id) |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | VoicePlayer.share().playerAt(url: weakSelf.listenNewModel.subjectList[weakSelf.page][index.row].correct) |
| | | } |
| | | }else{ |
| | | answer = .fail |
| | | VoicePlayer.share().playFailVoice() |
| | | self?.isListen = false |
| | | if self?.isAnsterComplete == false{ |
| | | self?.rootViewModel.errorNum += 1 |
| | | } |
| | | } |
| | | |
| | | switch answer { |
| | | case .success: |
| | | self?.viewModel.answerType.accept(.success) |
| | | self?.answerSuccess(cell) |
| | | case .fail: |
| | | self?.viewModel.answerType.accept(.fail) |
| | | self?.collectionView.reloadData() |
| | | default:break |
| | | } |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | //回答正确 |
| | | private func answerSuccess(_ cell:ListenFight_lesson_1_CCell){ |
| | | menuView?.snp.removeConstraints() |
| | | menuView?.playing() |
| | | menuView?.jq_cornerRadius = 0 |
| | | let v = cell.view_topHandle.convert(cell.bounds, to: self.view) |
| | | UIView.animate(withDuration: 0.3) { |
| | | self.menuView?.snp.updateConstraints { make in |
| | | make.top.equalTo(self.view).offset(v.origin.y + UIDevice.jq_safeEdges.top + 101 + 50) |
| | | make.left.equalToSuperview().offset(v.origin.x + 194) |
| | | make.width.equalTo(v.size.width - 10) |
| | | make.height.equalTo(40) |
| | | } |
| | | self.view.layoutIfNeeded() |
| | | }completion: { _ in |
| | | self.collectionView.reloadData() |
| | | } |
| | | } |
| | | func handleClouseAction(clouse:@escaping ()->Void){ |
| | | self.handleClouse = clouse |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_1_VC:UICollectionViewDelegate{ |
| | | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
| | | if !isListen{ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | viewModel.selectIndex.accept(indexPath) |
| | | } |
| | | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
| | | if !isListen{ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | viewModel.selectIndex.accept(indexPath) |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_1_VC:UICollectionViewDataSource{ |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_1_CCell", for: indexPath) as! ListenFight_lesson_1_CCell |
| | | cell.jq_addShadows(shadowColor: .black.withAlphaComponent(0.31), corner: 5, radius: 5, offset: CGSize(width: 0, height: 1), opacity: 1) |
| | | cell.backgroundColor = .white |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_1_CCell", for: indexPath) as! ListenFight_lesson_1_CCell |
| | | cell.jq_addShadows(shadowColor: .black.withAlphaComponent(0.31), corner: 5, radius: 5, offset: CGSize(width: 0, height: 1), opacity: 1) |
| | | cell.backgroundColor = .white |
| | | |
| | | if viewModel.selectIndex.value == indexPath{ |
| | | cell.setState(state: viewModel.answerType.value) |
| | | }else{ |
| | | cell.setState(state: .none) |
| | | } |
| | | cell.setListen1SubModel(listenNewModel.subjectList[page][indexPath.row]) |
| | | return cell |
| | | } |
| | | if viewModel.selectIndex.value == indexPath{ |
| | | cell.setState(state: viewModel.answerType.value) |
| | | }else{ |
| | | cell.setState(state: .none) |
| | | } |
| | | cell.setListen1SubModel(listenNewModel.subjectList[page][indexPath.row]) |
| | | return cell |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | return listenNewModel.subjectList[page].count |
| | | } |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | return listenNewModel.subjectList[page].count |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_1_VC:VoicePlayerDelegate{ |
| | | func playing() { |
| | | isListen = false |
| | | self.view.isUserInteractionEnabled = false |
| | | } |
| | | func playing() { |
| | | isListen = false |
| | | self.view.isUserInteractionEnabled = false |
| | | } |
| | | |
| | | func playComplete() { |
| | | isListen = true |
| | | self.view.isUserInteractionEnabled = true |
| | | self.menuView?.resetView() |
| | | func playComplete() { |
| | | isListen = true |
| | | self.view.isUserInteractionEnabled = true |
| | | self.menuView?.resetView() |
| | | |
| | | if isAnsterComplete{ |
| | | getNextAnswer() |
| | | } |
| | | } |
| | | if isAnsterComplete{ |
| | | getNextAnswer() |
| | | } |
| | | } |
| | | } |
| | |
| | | import UIKit |
| | | |
| | | enum Fight_lessonType { |
| | | case success |
| | | case fail |
| | | case none |
| | | case success |
| | | case fail |
| | | case none |
| | | } |
| | | |
| | | class ListenFight_lesson_1_CCell: UICollectionViewCell { |
| | | |
| | | @IBOutlet weak var label_title: UILabel! |
| | | @IBOutlet weak var image_state: UIImageView! |
| | | @IBOutlet weak var image_cover: UIImageView! |
| | | @IBOutlet weak var view_topHandle: UIView! |
| | | override func awakeFromNib() { |
| | | super.awakeFromNib() |
| | | image_state.alpha = 0 |
| | | image_state.transform = .init(scaleX: 0.1, y: 0.1) |
| | | label_title.isHidden = true |
| | | image_cover.contentMode = .scaleToFill |
| | | } |
| | | @IBOutlet weak var label_title: UILabel! |
| | | @IBOutlet weak var image_state: UIImageView! |
| | | @IBOutlet weak var image_cover: UIImageView! |
| | | @IBOutlet weak var view_topHandle: UIView! |
| | | override func awakeFromNib() { |
| | | super.awakeFromNib() |
| | | image_state.alpha = 0 |
| | | image_state.transform = .init(scaleX: 0.1, y: 0.1) |
| | | label_title.isHidden = true |
| | | image_cover.contentMode = .scaleToFill |
| | | } |
| | | |
| | | func setState(state:Fight_lessonType){ |
| | | func setState(state:Fight_lessonType){ |
| | | |
| | | switch state { |
| | | case .success: |
| | | image_state.image = UIImage(named: "icon_success") |
| | | UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.4, options: .layoutSubviews) { |
| | | self.image_state.alpha = 1 |
| | | self.image_state.transform = .init(scaleX: 1, y: 1) |
| | | } |
| | | UIView.animate(withDuration: 0.5, delay: 3.0) { |
| | | self.image_state.alpha = 0 |
| | | self.image_state.transform = .init(scaleX: 0.1, y: 0.1) |
| | | } |
| | | case .fail: |
| | | image_state.image = UIImage(named: "icon_fail") |
| | | UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.4, options: .layoutSubviews) { |
| | | self.image_state.alpha = 1 |
| | | self.image_state.transform = .init(scaleX: 1, y: 1) |
| | | UIView.animate(withDuration: 0.5, delay: 3.0) { |
| | | self.image_state.alpha = 0 |
| | | self.image_state.transform = .init(scaleX: 0.1, y: 0.1) |
| | | } |
| | | } |
| | | case .none: |
| | | image_state.alpha = 0 |
| | | image_state.transform = .init(scaleX: 0.1, y: 0.1) |
| | | } |
| | | } |
| | | switch state { |
| | | case .success: |
| | | image_state.image = UIImage(named: "icon_success") |
| | | UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.4, options: .layoutSubviews) { |
| | | self.image_state.alpha = 1 |
| | | self.image_state.transform = .init(scaleX: 1, y: 1) |
| | | } |
| | | UIView.animate(withDuration: 0.5, delay: 3.0) { |
| | | self.image_state.alpha = 0 |
| | | self.image_state.transform = .init(scaleX: 0.1, y: 0.1) |
| | | } |
| | | case .fail: |
| | | image_state.image = UIImage(named: "icon_fail") |
| | | UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.4, options: .layoutSubviews) { |
| | | self.image_state.alpha = 1 |
| | | self.image_state.transform = .init(scaleX: 1, y: 1) |
| | | UIView.animate(withDuration: 0.5, delay: 3.0) { |
| | | self.image_state.alpha = 0 |
| | | self.image_state.transform = .init(scaleX: 0.1, y: 0.1) |
| | | } |
| | | } |
| | | case .none: |
| | | image_state.alpha = 0 |
| | | image_state.transform = .init(scaleX: 0.1, y: 0.1) |
| | | } |
| | | } |
| | | |
| | | func setListen1SubModel(_ model:Listen1SubModel){ |
| | | image_cover.sd_setImage(with: URL(string: model.img)) |
| | | } |
| | | func setListen1SubModel(_ model:Listen1SubModel){ |
| | | image_cover.sd_setImage(with: URL(string: model.img)) |
| | | } |
| | | } |
| | |
| | | <subviews> |
| | | <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="--" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rgH-1a-mKt"> |
| | | <rect key="frame" x="0.0" y="0.0" width="435" height="40"/> |
| | | <color key="backgroundColor" red="0.53725490196078429" green="0.52941176470588236" blue="0.49411764705882355" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> |
| | | <fontDescription key="fontDescription" type="system" weight="medium" pointSize="16"/> |
| | | <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> |
| | | <color key="textColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> |
| | | <nil key="highlightedColor"/> |
| | | </label> |
| | | </subviews> |
| | | <color key="backgroundColor" red="0.83137254900000002" green="0.82352941180000006" blue="0.80392156859999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> |
| | | <constraints> |
| | | <constraint firstAttribute="height" constant="40" id="1mb-ik-h6n"/> |
| | | <constraint firstAttribute="trailing" secondItem="rgH-1a-mKt" secondAttribute="trailing" id="Zjf-E9-9in"/> |
| | |
| | | |
| | | class ListenFight_lesson_3_CCell: UICollectionViewCell { |
| | | |
| | | @IBOutlet weak var img_cover: UIImageView! |
| | | @IBOutlet weak var view_container: UIView! |
| | | @IBOutlet weak var btn_play: UIButton! |
| | | @IBOutlet weak var btn_playing: UIButton! |
| | | @IBOutlet weak var view_playHandle: UIView! |
| | | @IBOutlet weak var img_playing: UIImageView! |
| | | @IBOutlet weak var img_playSuccess: UIImageView! |
| | | |
| | | private var model:Listen1SubModel! |
| | | private var playAtClouse:((IndexPath)->Void)? |
| | | var indexPath:IndexPath! |
| | | @IBOutlet weak var img_cover: UIImageView! |
| | | @IBOutlet weak var view_container: UIView! |
| | | @IBOutlet weak var btn_play: UIButton! |
| | | @IBOutlet weak var btn_playing: UIButton! |
| | | @IBOutlet weak var view_playHandle: UIView! |
| | | @IBOutlet weak var img_playing: UIImageView! |
| | | |
| | | override func awakeFromNib() { |
| | | private var model:Listen1SubModel! |
| | | private var playAtClouse:((IndexPath)->Void)? |
| | | var indexPath:IndexPath! |
| | | |
| | | override func awakeFromNib() { |
| | | super.awakeFromNib() |
| | | view_container.jq_addShadows(shadowColor: .black.withAlphaComponent(0.31), corner: 8, radius: 3, offset: CGSize(width: 0, height: 1), opacity: 1) |
| | | layoutIfNeeded() |
| | | layoutIfNeeded() |
| | | |
| | | btn_playing.setImage(UIImage(named: "icon_play_1"), for: .normal) |
| | | btn_play.setImage(UIImage(named: "icon_play"), for: .normal) |
| | | img_playing.image = UIImage(named: "icon_playing")?.themeGreen |
| | | view_playHandle.backgroundColor = .white |
| | | } |
| | | |
| | | func setModel(_ model:Listen1SubModel,isplaying:Bool){ |
| | | self.model = model |
| | | self.btn_play.alpha = (isplaying ? 0:1) |
| | | self.btn_playing.alpha = (isplaying ? 0:1) |
| | | self.img_playing.alpha = (isplaying ? 1:0) |
| | | } |
| | | override func layoutSubviews() { |
| | | super.layoutSubviews() |
| | | // jq_addShadows(shadowColor: .black.withAlphaComponent(0.31), corner: 8, radius: 3, offset: CGSize(width: 0, height: 1), opacity: 1) |
| | | jq_cornerRadius = 8 |
| | | } |
| | | |
| | | func palyVoiceAt(_ clouse:@escaping(IndexPath)->Void){ |
| | | self.playAtClouse = clouse |
| | | } |
| | | func setModel(_ model:Listen1SubModel,isplaying:Bool){ |
| | | self.model = model |
| | | self.btn_play.alpha = (isplaying ? 0:1) |
| | | self.btn_playing.alpha = (isplaying ? 0:1) |
| | | self.img_playing.alpha = (isplaying ? 1:0) |
| | | } |
| | | |
| | | func canClick(_ state:Bool){ |
| | | btn_play.isEnabled = state |
| | | view_playHandle.backgroundColor = state == true ? UIColor(hexString: "#41A2EB") : .gray |
| | | } |
| | | func palyVoiceAt(_ clouse:@escaping(IndexPath)->Void){ |
| | | self.playAtClouse = clouse |
| | | } |
| | | |
| | | func isPlaying(isplaying:Bool){ |
| | | btn_play.alpha = (isplaying ? 0:1) |
| | | btn_playing.alpha = (isplaying ? 0:1) |
| | | img_playing.alpha = (isplaying ? 1:0) |
| | | } |
| | | func canClick(_ state:Bool){ |
| | | btn_play.isEnabled = state |
| | | view_playHandle.backgroundColor = .white |
| | | } |
| | | |
| | | // func playSuccess(){ |
| | | // UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.4, options: .layoutSubviews) { |
| | | // self.img_playSuccess.alpha = 1 |
| | | // self.img_playSuccess.transform = .init(scaleX: 1, y: 1) |
| | | // } |
| | | // UIView.animate(withDuration: 0.5, delay: 3.0) { |
| | | // self.img_playSuccess.alpha = 0 |
| | | // self.img_playSuccess.transform = .init(scaleX: 0.1, y: 0.1) |
| | | // } |
| | | // } |
| | | func isPlaying(isplaying:Bool){ |
| | | btn_play.alpha = (isplaying ? 0:1) |
| | | btn_playing.alpha = (isplaying ? 0:1) |
| | | img_playing.alpha = (isplaying ? 1:0) |
| | | } |
| | | |
| | | |
| | | @IBAction func playAction(_ sender: Any) { |
| | | playAtClouse?(indexPath) |
| | | } |
| | | @IBAction func playAction(_ sender: Any) { |
| | | playAtClouse?(indexPath) |
| | | } |
| | | } |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> |
| | | <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> |
| | | <device id="ipad10_9rounded" orientation="portrait" layout="fullscreen" appearance="light"/> |
| | | <dependencies> |
| | | <deployment identifier="iOS"/> |
| | | <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/> |
| | | <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/> |
| | | <capability name="System colors in document resources" minToolsVersion="11.0"/> |
| | | <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> |
| | | </dependencies> |
| | |
| | | <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> |
| | | <subviews> |
| | | <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vtr-2e-B5C"> |
| | | <rect key="frame" x="0.0" y="0.0" width="159" height="52"/> |
| | | <rect key="frame" x="0.0" y="0.0" width="630" height="40"/> |
| | | <subviews> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="7yc-PU-RgV"> |
| | | <rect key="frame" x="104" y="10" width="32" height="32"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_play"/> |
| | | <connections> |
| | | <action selector="playAction:" destination="gTV-IL-0wX" eventType="touchUpInside" id="IWn-Zu-bCh"/> |
| | | </connections> |
| | | </button> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9Dc-Ns-SMP"> |
| | | <rect key="frame" x="25" y="12.5" width="27" height="27"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_play_1"/> |
| | | </button> |
| | | <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="83" translatesAutoresizingMaskIntoConstraints="NO" id="MXy-1Z-w1c"> |
| | | <rect key="frame" x="228.5" y="0.0" width="173" height="40"/> |
| | | <subviews> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="7yc-PU-RgV"> |
| | | <rect key="frame" x="0.0" y="0.0" width="45" height="40"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_play"/> |
| | | <connections> |
| | | <action selector="playAction:" destination="gTV-IL-0wX" eventType="touchUpInside" id="IWn-Zu-bCh"/> |
| | | </connections> |
| | | </button> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9Dc-Ns-SMP"> |
| | | <rect key="frame" x="128" y="0.0" width="45" height="40"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_play_1"/> |
| | | </button> |
| | | </subviews> |
| | | </stackView> |
| | | <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon_playing" translatesAutoresizingMaskIntoConstraints="NO" id="vMh-x4-Y7a"> |
| | | <rect key="frame" x="57" y="10.5" width="45" height="31"/> |
| | | <rect key="frame" x="292.5" y="4.5" width="45" height="31"/> |
| | | </imageView> |
| | | </subviews> |
| | | <color key="backgroundColor" red="0.25490196078431371" green="0.63529411764705879" blue="0.92156862745098034" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | <constraints> |
| | | <constraint firstItem="9Dc-Ns-SMP" firstAttribute="centerY" secondItem="vtr-2e-B5C" secondAttribute="centerY" id="C8e-Iq-OaB"/> |
| | | <constraint firstItem="7yc-PU-RgV" firstAttribute="centerY" secondItem="vtr-2e-B5C" secondAttribute="centerY" id="EUe-pu-sAP"/> |
| | | <constraint firstAttribute="bottom" secondItem="MXy-1Z-w1c" secondAttribute="bottom" id="LoX-sU-g5l"/> |
| | | <constraint firstItem="vMh-x4-Y7a" firstAttribute="centerX" secondItem="vtr-2e-B5C" secondAttribute="centerX" id="ObG-a0-OcJ"/> |
| | | <constraint firstAttribute="trailing" secondItem="7yc-PU-RgV" secondAttribute="trailing" constant="23" id="Rms-9S-fkG"/> |
| | | <constraint firstAttribute="height" constant="52" id="Y2Z-EL-K2n"/> |
| | | <constraint firstAttribute="width" constant="159" id="oI7-Oh-ubD"/> |
| | | <constraint firstItem="9Dc-Ns-SMP" firstAttribute="leading" secondItem="vtr-2e-B5C" secondAttribute="leading" constant="25" id="xRi-cc-X9V"/> |
| | | <constraint firstItem="MXy-1Z-w1c" firstAttribute="top" secondItem="vtr-2e-B5C" secondAttribute="top" id="RGy-nc-L5R"/> |
| | | <constraint firstAttribute="height" constant="40" id="Y2Z-EL-K2n"/> |
| | | <constraint firstItem="MXy-1Z-w1c" firstAttribute="centerX" secondItem="vtr-2e-B5C" secondAttribute="centerX" id="v69-Hn-XdH"/> |
| | | <constraint firstItem="vMh-x4-Y7a" firstAttribute="centerY" secondItem="vtr-2e-B5C" secondAttribute="centerY" id="xql-kz-i8e"/> |
| | | </constraints> |
| | | <userDefinedRuntimeAttributes> |
| | | <userDefinedRuntimeAttribute type="boolean" keyPath="ld_maskToBoundsXIB" value="YES"/> |
| | | <userDefinedRuntimeAttribute type="number" keyPath="ld_cornerRadiusXIB"> |
| | | <real key="value" value="8"/> |
| | | </userDefinedRuntimeAttribute> |
| | | </userDefinedRuntimeAttributes> |
| | | </view> |
| | | <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="qxz-6s-e5b"> |
| | | <rect key="frame" x="0.0" y="77" width="630" height="467"/> |
| | | <rect key="frame" x="0.0" y="40" width="630" height="504"/> |
| | | <subviews> |
| | | <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="n5n-eb-5xI"> |
| | | <rect key="frame" x="5" y="5" width="620" height="457"/> |
| | | <rect key="frame" x="5" y="5" width="620" height="494"/> |
| | | <color key="backgroundColor" red="0.94509803921568625" green="0.94509803921568625" blue="0.94509803921568625" alpha="0.84999999999999998" colorSpace="custom" customColorSpace="sRGB"/> |
| | | </imageView> |
| | | </subviews> |
| | |
| | | </view> |
| | | <constraints> |
| | | <constraint firstAttribute="trailing" secondItem="qxz-6s-e5b" secondAttribute="trailing" id="HMD-Sa-FHB"/> |
| | | <constraint firstAttribute="trailing" secondItem="vtr-2e-B5C" secondAttribute="trailing" id="LSt-bv-htS"/> |
| | | <constraint firstItem="qxz-6s-e5b" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="SLN-pI-I4q"/> |
| | | <constraint firstItem="vtr-2e-B5C" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="enY-sX-Oib"/> |
| | | <constraint firstItem="qxz-6s-e5b" firstAttribute="top" secondItem="vtr-2e-B5C" secondAttribute="bottom" constant="25" id="iLs-4C-NaW"/> |
| | | <constraint firstItem="qxz-6s-e5b" firstAttribute="top" secondItem="vtr-2e-B5C" secondAttribute="bottom" id="iLs-4C-NaW"/> |
| | | <constraint firstItem="vtr-2e-B5C" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="jbw-ZJ-YrV"/> |
| | | <constraint firstAttribute="bottom" secondItem="qxz-6s-e5b" secondAttribute="bottom" constant="10" id="msT-T9-1c4"/> |
| | | </constraints> |
| | |
| | | </collectionViewCell> |
| | | </objects> |
| | | <resources> |
| | | <image name="icon_play" width="32" height="32"/> |
| | | <image name="icon_play_1" width="27" height="27"/> |
| | | <image name="icon_play" width="45" height="45"/> |
| | | <image name="icon_play_1" width="28.5" height="30"/> |
| | | <image name="icon_playing" width="45" height="31"/> |
| | | <systemColor name="systemBackgroundColor"> |
| | | <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> |
| | |
| | | // |
| | | |
| | | import UIKit |
| | | import JQTools |
| | | |
| | | class ListenFight_lesson_4_CCell: UICollectionViewCell { |
| | | |
| | |
| | | |
| | | override func awakeFromNib() { |
| | | super.awakeFromNib() |
| | | view_container.jq_addShadows(shadowColor: .black.withAlphaComponent(0.31), corner: 8, radius: 3, offset: CGSize(width: 0, height: 1), opacity: 1) |
| | | view_handle.backgroundColor = .white |
| | | btn_handle.setImage(UIImage(named: "icon_question"), for: .normal) |
| | | btn_play.setImage(UIImage(named: "icon_play"), for: .normal) |
| | | btn_voice.setImage(UIImage(named: "icon_play_1"), for: .normal) |
| | | btn_handle.isUserInteractionEnabled = true |
| | | } |
| | | |
| | | override func layoutSubviews() { |
| | | super.layoutSubviews() |
| | | // jq_addShadows(shadowColor: .black.withAlphaComponent(0.31), corner: 8, radius: 3, offset: CGSize(width: 0, height: 1), opacity: 1) |
| | | |
| | | jq_cornerRadius = 8 |
| | | } |
| | | |
| | | func setModel(_ m:Listen1SubModel){ |
| | | model = m |
| | |
| | | } |
| | | |
| | | func playing(){ |
| | | btn_handle.isHidden = true |
| | | // btn_handle.isHidden = true |
| | | btn_play.isHidden = true |
| | | btn_voice.setImage(UIImage(named: "icon_playing"), for: .normal) |
| | | btn_voice.setImage(UIImage(named: "icon_playing"), for: .normal) |
| | | } |
| | | |
| | | func playEnd(){ |
| | | btn_handle.isHidden = false |
| | | // btn_handle.isHidden = false |
| | | btn_play.isHidden = false |
| | | btn_voice.setImage(UIImage(named: "icon_play_1"), for: .normal) |
| | | btn_voice.setImage(UIImage(named: "icon_play_1"), for: .normal) |
| | | } |
| | | |
| | | @IBAction func playAction(_ sender: TapBtn) { |
| | | @IBAction func playAction(_ sender: UIButton) { |
| | | if let m = model{ |
| | | VoicePlayer.share().playerAt(url: m.correct) |
| | | playAtIndexClouse?(indexPath) |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> |
| | | <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> |
| | | <device id="ipad10_9rounded" orientation="portrait" layout="fullscreen" appearance="light"/> |
| | | <dependencies> |
| | | <deployment identifier="iOS"/> |
| | | <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/> |
| | | <capability name="Safe area layout guides" minToolsVersion="9.0"/> |
| | | <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/> |
| | | <capability name="System colors in document resources" minToolsVersion="11.0"/> |
| | | <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> |
| | | </dependencies> |
| | |
| | | <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> |
| | | <subviews> |
| | | <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Xjz-V8-keG" customClass="TapBtn" customModule="DolphinEnglishLearnStudent" customModuleProvider="target"> |
| | | <rect key="frame" x="0.0" y="0.0" width="159" height="52"/> |
| | | <rect key="frame" x="0.0" y="0.0" width="597" height="40"/> |
| | | <subviews> |
| | | <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="LLy-9v-eQJ"> |
| | | <rect key="frame" x="105" y="10" width="31" height="32"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_play"/> |
| | | </button> |
| | | <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="kgj-Ss-D90"> |
| | | <rect key="frame" x="66" y="12.5" width="27" height="27"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_play_1"/> |
| | | </button> |
| | | <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ZuK-r9-26C"> |
| | | <rect key="frame" x="15" y="9.5" width="33" height="33"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_question"/> |
| | | </button> |
| | | <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="EtJ-zc-pEe"> |
| | | <rect key="frame" x="20" y="0.0" width="557" height="40"/> |
| | | <subviews> |
| | | <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ZuK-r9-26C"> |
| | | <rect key="frame" x="0.0" y="0.0" width="185.5" height="40"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_question"/> |
| | | </button> |
| | | <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="kgj-Ss-D90"> |
| | | <rect key="frame" x="185.5" y="0.0" width="186" height="40"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_play_1"/> |
| | | </button> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="LLy-9v-eQJ"> |
| | | <rect key="frame" x="371.5" y="0.0" width="185.5" height="40"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_play"/> |
| | | <connections> |
| | | <action selector="playAction:" destination="gTV-IL-0wX" eventType="touchUpInside" id="vty-Fu-kuW"/> |
| | | </connections> |
| | | </button> |
| | | </subviews> |
| | | </stackView> |
| | | </subviews> |
| | | <color key="backgroundColor" red="0.25490196079999999" green="0.63529411759999999" blue="0.92156862750000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> |
| | | <constraints> |
| | | <constraint firstAttribute="width" constant="159" id="7Mr-KM-NTE"/> |
| | | <constraint firstAttribute="trailing" secondItem="LLy-9v-eQJ" secondAttribute="trailing" constant="23" id="9dR-Nl-sPa"/> |
| | | <constraint firstItem="ZuK-r9-26C" firstAttribute="centerY" secondItem="Xjz-V8-keG" secondAttribute="centerY" id="Erm-17-erv"/> |
| | | <constraint firstItem="LLy-9v-eQJ" firstAttribute="centerY" secondItem="Xjz-V8-keG" secondAttribute="centerY" id="FB0-fx-81Z"/> |
| | | <constraint firstItem="LLy-9v-eQJ" firstAttribute="leading" secondItem="kgj-Ss-D90" secondAttribute="trailing" constant="12" id="MoJ-Ne-mlq"/> |
| | | <constraint firstAttribute="height" constant="52" id="gfp-ph-1NP"/> |
| | | <constraint firstItem="kgj-Ss-D90" firstAttribute="leading" secondItem="ZuK-r9-26C" secondAttribute="trailing" constant="18" id="qM1-Yp-Nha"/> |
| | | <constraint firstItem="kgj-Ss-D90" firstAttribute="centerX" secondItem="Xjz-V8-keG" secondAttribute="centerX" id="x1s-RS-WOb"/> |
| | | <constraint firstItem="kgj-Ss-D90" firstAttribute="centerY" secondItem="Xjz-V8-keG" secondAttribute="centerY" id="zxl-IP-PhN"/> |
| | | <constraint firstAttribute="bottom" secondItem="EtJ-zc-pEe" secondAttribute="bottom" id="4rh-Rk-UOs"/> |
| | | <constraint firstItem="EtJ-zc-pEe" firstAttribute="leading" secondItem="Xjz-V8-keG" secondAttribute="leading" constant="20" id="5Nn-r4-YwD"/> |
| | | <constraint firstItem="EtJ-zc-pEe" firstAttribute="top" secondItem="Xjz-V8-keG" secondAttribute="top" id="Ri7-jh-mGE"/> |
| | | <constraint firstAttribute="height" constant="40" id="gfp-ph-1NP"/> |
| | | <constraint firstAttribute="trailing" secondItem="EtJ-zc-pEe" secondAttribute="trailing" constant="20" id="j8s-7c-9su"/> |
| | | </constraints> |
| | | <userDefinedRuntimeAttributes> |
| | | <userDefinedRuntimeAttribute type="boolean" keyPath="ld_maskToBoundsXIB" value="YES"/> |
| | | <userDefinedRuntimeAttribute type="number" keyPath="ld_cornerRadiusXIB"> |
| | | <real key="value" value="8"/> |
| | | </userDefinedRuntimeAttribute> |
| | | </userDefinedRuntimeAttributes> |
| | | <connections> |
| | | <action selector="playAction:" destination="gTV-IL-0wX" eventType="touchUpInside" id="8ns-md-QJB"/> |
| | | <action selector="playAction:" destination="gTV-IL-0wX" eventType="valueChanged" id="7bY-LY-aXz"/> |
| | | </connections> |
| | | </view> |
| | | <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="OJ6-0b-fVC"> |
| | | <rect key="frame" x="0.0" y="62" width="597" height="438"/> |
| | | <rect key="frame" x="0.0" y="40" width="597" height="460"/> |
| | | <subviews> |
| | | <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="XW5-ds-CXG"> |
| | | <rect key="frame" x="5" y="5" width="587" height="428"/> |
| | | <rect key="frame" x="5" y="5" width="587" height="450"/> |
| | | <color key="backgroundColor" red="0.94509803920000002" green="0.94509803920000002" blue="0.94509803920000002" alpha="0.84999999999999998" colorSpace="custom" customColorSpace="sRGB"/> |
| | | </imageView> |
| | | <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon_success" translatesAutoresizingMaskIntoConstraints="NO" id="eyJ-Qy-E0w"> |
| | | <rect key="frame" x="258.5" y="178.5" width="80" height="81"/> |
| | | <rect key="frame" x="258.5" y="189.5" width="80" height="81"/> |
| | | </imageView> |
| | | </subviews> |
| | | <color key="backgroundColor" systemColor="systemBackgroundColor"/> |
| | |
| | | </view> |
| | | </subviews> |
| | | </view> |
| | | <viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/> |
| | | <constraints> |
| | | <constraint firstAttribute="trailing" secondItem="Xjz-V8-keG" secondAttribute="trailing" id="0zx-cJ-mwb"/> |
| | | <constraint firstItem="Xjz-V8-keG" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="3SB-P6-hOR"/> |
| | | <constraint firstItem="Xjz-V8-keG" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="5Iq-AY-uUK"/> |
| | | <constraint firstItem="OJ6-0b-fVC" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="6p8-30-BD6"/> |
| | | <constraint firstAttribute="trailing" secondItem="OJ6-0b-fVC" secondAttribute="trailing" id="EAu-4C-peY"/> |
| | | <constraint firstItem="OJ6-0b-fVC" firstAttribute="top" secondItem="Xjz-V8-keG" secondAttribute="bottom" constant="10" id="YpO-u8-Uyg"/> |
| | | <constraint firstItem="OJ6-0b-fVC" firstAttribute="top" secondItem="Xjz-V8-keG" secondAttribute="bottom" id="YpO-u8-Uyg"/> |
| | | <constraint firstAttribute="bottom" secondItem="OJ6-0b-fVC" secondAttribute="bottom" constant="10" id="m6e-nJ-ukf"/> |
| | | </constraints> |
| | | <size key="customSize" width="597" height="510"/> |
| | |
| | | </collectionViewCell> |
| | | </objects> |
| | | <resources> |
| | | <image name="icon_play" width="32" height="32"/> |
| | | <image name="icon_play_1" width="27" height="27"/> |
| | | <image name="icon_question" width="33" height="33"/> |
| | | <image name="icon_play" width="45" height="45"/> |
| | | <image name="icon_play_1" width="28.5" height="30"/> |
| | | <image name="icon_question" width="45" height="45"/> |
| | | <image name="icon_success" width="80" height="81"/> |
| | | <systemColor name="systemBackgroundColor"> |
| | | <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> |
| | |
| | | import UIKit |
| | | |
| | | class HomeListen_item_TCell: UITableViewCell { |
| | | @IBOutlet weak var view_bg1: UIView! |
| | | @IBOutlet weak var view_bg2: UIView! |
| | | @IBOutlet weak var label_title: UILabel! |
| | | @IBOutlet weak var view_state: UIView! |
| | | @IBOutlet weak var label_state: UILabel! |
| | | |
| | | @IBOutlet weak var view_bg1: UIView! |
| | | @IBOutlet weak var view_bg2: UIView! |
| | | @IBOutlet weak var label_title: UILabel! |
| | | @IBOutlet weak var view_state: UIView! |
| | | @IBOutlet weak var label_state: UILabel! |
| | | |
| | | override func awakeFromNib() { |
| | | super.awakeFromNib() |
| | | backgroundColor = .clear |
| | | selectionStyle = .none |
| | | backgroundColor = .clear |
| | | selectionStyle = .none |
| | | } |
| | | |
| | | func setProgress(progress:Int){ |
| | | if progress <= 0{ |
| | | view_state.backgroundColor = UIColor(hexString: "#F84D31") |
| | | label_state.text = "未完成" |
| | | }else if progress == 100{ |
| | | view_state.backgroundColor = UIColor(hexString: "#66CFFA") |
| | | label_state.text = "已完成" |
| | | }else if progress > 0 && progress < 100{ |
| | | view_state.backgroundColor = UIColor(hexString: "#FF8A66") |
| | | label_state.text = String(format: "剩余:%ld%%", progress) |
| | | } |
| | | func setProgress(progress:Int){ |
| | | if progress <= 0{ |
| | | view_state.backgroundColor = UIColor(hexString: "#F84D31") |
| | | label_state.text = "未完成" |
| | | }else if progress == 100{ |
| | | view_state.backgroundColor = UIColor(hexString: "#66CFFA") |
| | | label_state.text = "已完成" |
| | | }else if progress > 0 && progress < 100{ |
| | | view_state.backgroundColor = UIColor(hexString: "#FF8A66") |
| | | label_state.text = String(format: "剩余:%ld%%", progress) |
| | | } |
| | | |
| | | } |
| | | } |
| | | } |
| | |
| | | import RxRelay |
| | | |
| | | let NextLession_Noti = Notification.Name.init("NextLession_Noti") |
| | | let ResetLession_Noti = Notification.Name.init("ResetLession_Noti") |
| | | //let Reload_Noti = Notification.Name.init("Reload_Noti") |
| | | |
| | | enum ListenType:Int{ |
| | | case lesson1 = 1 //自主学习-听音选图 |
| | | case lesson2 = 2 //自主学习-看图选音 |
| | | case lesson3 = 3 //自主学习-归纳排除 |
| | | case lesson4 = 4 //自主学习-有问有答 |
| | | case lesson5 = 5 //自主学习-音图相配 |
| | | case game1 = 6 //游戏类型-超级听力 |
| | | case game2 = 7 //游戏类型-超级记忆 |
| | | case story1 = 8 //故事类型-自主故事1-看图配音 |
| | | case story2 = 9 //故事类型-自主故事2-框架记忆 |
| | | case lesson1 = 1 //自主学习-听音选图 |
| | | case lesson2 = 2 //自主学习-看图选音 |
| | | case lesson3 = 3 //自主学习-归纳排除 |
| | | case lesson4 = 4 //自主学习-有问有答 |
| | | case lesson5 = 5 //自主学习-音图相配 |
| | | case game1 = 6 //游戏类型-超级听力 |
| | | case game2 = 7 //游戏类型-超级记忆 |
| | | case story1 = 8 //故事类型-自主故事1-看图配音 |
| | | case story2 = 9 //故事类型-自主故事2-框架记忆 |
| | | |
| | | var rawTitle:String{ |
| | | switch self { |
| | | case .lesson1:return "自主学习1-听音选图" |
| | | case .lesson2:return "自主学习2-看图选音" |
| | | case .lesson3:return "自主学习3-归纳排除" |
| | | case .lesson4:return "自主学习4-有问有答" |
| | | case .lesson5:return "自主学习5-音图相配" |
| | | case .game1:return "游戏类型1-超级听力" |
| | | case .game2:return "游戏类型2-超级记忆" |
| | | case .story1:return "自主故事1-看图配音" |
| | | case .story2:return "自主故事2-框架记忆" |
| | | } |
| | | } |
| | | var rawTitle:String{ |
| | | switch self { |
| | | case .lesson1:return "自主学习1-听音选图" |
| | | case .lesson2:return "自主学习2-看图选音" |
| | | case .lesson3:return "自主学习3-归纳排除" |
| | | case .lesson4:return "自主学习4-有问有答" |
| | | case .lesson5:return "自主学习5-音图相配" |
| | | case .game1:return "游戏类型1-超级听力" |
| | | case .game2:return "游戏类型2-超级记忆" |
| | | case .story1:return "自主故事1-看图配音" |
| | | case .story2:return "自主故事2-框架记忆" |
| | | } |
| | | } |
| | | } |
| | | |
| | | enum ListenFightLine{ |
| | | case before |
| | | case next |
| | | case none |
| | | case before |
| | | case next |
| | | case none |
| | | } |
| | | |
| | | |
| | | //中途退出所需要 |
| | | class ExitLearnModel{ |
| | | var topicsIds = Set<Int>() |
| | | var topicsIds = Set<Int>() |
| | | } |
| | | |
| | | class HomeListenFightViewModel{ |
| | | |
| | | /// 当前页数 |
| | | var currentPage = BehaviorRelay<Int>(value: 0) |
| | | var subPage = BehaviorRelay<Int>(value: 1) //小题目角标 |
| | | var maxPage = BehaviorRelay<Int>(value: 5) |
| | | var listenType = BehaviorRelay<ListenType>(value:.lesson1) |
| | | var times:Int = 0 |
| | | var quarter = BehaviorRelay<Int?>(value: 0) |
| | | var week = BehaviorRelay<Int?>(value: 0) |
| | | var day = BehaviorRelay<Int?>(value: 0) |
| | | /// 当前页数 |
| | | var currentPage = BehaviorRelay<Int>(value: 0) |
| | | // var subPage = BehaviorRelay<Int>(value: 1) //小题目角标 |
| | | var maxPage = BehaviorRelay<Int>(value: 5) |
| | | var listenType = BehaviorRelay<ListenType>(value:.lesson1) |
| | | var times:Int = 0 |
| | | var quarter = BehaviorRelay<Int?>(value: 0) |
| | | var week = BehaviorRelay<Int?>(value: 0) |
| | | var day = BehaviorRelay<Int?>(value: 0) |
| | | |
| | | //游戏专属,游戏等级 |
| | | var gameLevel = BehaviorRelay<Int>(value:0) |
| | | //游戏专属,游戏等级 |
| | | var gameLevel = BehaviorRelay<Int>(value:0) |
| | | |
| | | //回答错误数量 |
| | | var correctNum:Int = 0{ |
| | | didSet{ |
| | | print("回答正确:\(correctNum)") |
| | | } |
| | | } |
| | | //回答错误数量 |
| | | var correctNum:Int = 0{ |
| | | didSet{ |
| | | print("回答正确:\(correctNum)") |
| | | } |
| | | } |
| | | |
| | | /// 回答错误数量 |
| | | var errorNum:Int = 0{ |
| | | didSet{ |
| | | print("回答错误:\(correctNum)") |
| | | } |
| | | } |
| | | /// 回答错误数量 |
| | | var errorNum:Int = 0{ |
| | | didSet{ |
| | | print("回答错误:\(correctNum)") |
| | | } |
| | | } |
| | | |
| | | //所有回答的 两游戏在用 |
| | | var answerItems = Dictionary<Int,Any>() //{page:0,data:String,currectAt:0} |
| | | var answerCount = BehaviorRelay<Int>(value: 1) |
| | | //所有回答的 两游戏在用 |
| | | var answerItems = Dictionary<Int,Any>() //{page:0,data:String,currectAt:0} |
| | | var answerCount = BehaviorRelay<Int>(value: 1) |
| | | |
| | | var answerItems_1 = Dictionary<String,Array<Int>>() |
| | | var answerItems_1 = Dictionary<String,Array<Int>>() |
| | | |
| | | //回答正确的题 |
| | | func insertCorrectAnswer(teamId:String?,answerId:Int){ |
| | | guard teamId != nil else {return} |
| | | if answerItems_1[teamId!] == nil{ |
| | | answerItems_1[teamId!] = Array<Int>() |
| | | } |
| | | //回答正确的题 |
| | | func insertCorrectAnswer(teamId:String?,answerId:Int){ |
| | | guard teamId != nil else {return} |
| | | if answerItems_1[teamId!] == nil{ |
| | | answerItems_1[teamId!] = Array<Int>() |
| | | } |
| | | |
| | | answerItems_1[teamId!]!.append(answerId) |
| | | } |
| | | answerItems_1[teamId!]!.append(answerId) |
| | | } |
| | | } |
| | | |
| | | class HomeListenFightVC: BaseVC { |
| | | private var viewModel = HomeListenFightViewModel() |
| | | var studyScheduleModel:StudyScheduleModel? //学习进度(上级传递) |
| | | var listenFightLine:ListenFightLine = .none |
| | | var data:Any? |
| | | private var viewModel = HomeListenFightViewModel() |
| | | var studyScheduleModel:StudyScheduleModel? //学习进度(上级传递) |
| | | var listenFightLine:ListenFightLine = .none |
| | | var data:Any? |
| | | |
| | | var maxPage = 0 //最大页记录 |
| | | var teamScheduleModel:TeamScheduleModel? //上次中途退出,答题记录 |
| | | var maxPage = 0 //最大页记录 |
| | | var teamScheduleModel:TeamScheduleModel? //上次中途退出,答题记录 |
| | | var pages = [[ListenSubCardModel]]() //分页显示 |
| | | |
| | | private var notiObject:Dictionary<String,Any>? |
| | | private var notiObject:Dictionary<String,Any>? |
| | | |
| | | private lazy var label_pageNum:UILabel = { |
| | | let label = UILabel() |
| | | label.font = .systemFont(ofSize: 14, weight: .medium) |
| | | label.textColor = .black.withAlphaComponent(0.81) |
| | | label.textAlignment = .center |
| | | label.text = "已完成:0/0" |
| | | return label |
| | | }() |
| | | private lazy var label_pageNum:UILabel = { |
| | | let label = UILabel() |
| | | label.font = .systemFont(ofSize: 14, weight: .medium) |
| | | label.textColor = .black.withAlphaComponent(0.81) |
| | | label.textAlignment = .center |
| | | label.numberOfLines = 2 |
| | | label.text = "已完成:0/0\n正确率:--%" |
| | | return label |
| | | }() |
| | | |
| | | private lazy var btn_forward:UIButton = { |
| | | let btn = UIButton(type: .custom) |
| | | btn.setTitle("上一题", for: .normal) |
| | | btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) |
| | | btn.setTitleColor(Config.ThemeColor, for: .normal) |
| | | btn.jq_borderColor = Config.ThemeColor |
| | | btn.backgroundColor = .white |
| | | btn.jq_borderWidth = 1 |
| | | btn.jq_cornerRadius = 4 |
| | | return btn |
| | | }() |
| | | private lazy var btn_forward:UIButton = { |
| | | let btn = UIButton(type: .custom) |
| | | btn.setTitle("上一题", for: .normal) |
| | | btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) |
| | | btn.setTitleColor(Config.ThemeColor, for: .normal) |
| | | btn.jq_borderColor = Config.ThemeColor |
| | | btn.backgroundColor = .white |
| | | btn.jq_borderWidth = 1 |
| | | btn.jq_cornerRadius = 4 |
| | | return btn |
| | | }() |
| | | |
| | | private lazy var btn_forward_mini:UIButton = { |
| | | let btn = UIButton(type: .custom) |
| | | btn.setTitle("上一小题", for: .normal) |
| | | btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) |
| | | btn.setTitleColor(Config.ThemeColor, for: .normal) |
| | | btn.jq_borderColor = Config.ThemeColor |
| | | btn.backgroundColor = .white |
| | | btn.isHidden = true |
| | | btn.jq_borderWidth = 1 |
| | | btn.jq_cornerRadius = 4 |
| | | return btn |
| | | }() |
| | | private lazy var collection_card:UICollectionView = { |
| | | let layout = UICollectionViewFlowLayout() |
| | | layout.minimumInteritemSpacing = 2 |
| | | layout.minimumLineSpacing = 2 |
| | | layout.itemSize = CGSize(width: 24, height: 24) |
| | | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) |
| | | collectionView.backgroundColor = .clear |
| | | return collectionView |
| | | }() |
| | | |
| | | private lazy var btn_next:UIButton = { |
| | | let btn = UIButton(type: .custom) |
| | | btn.setTitle("下一题", for: .normal) |
| | | btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) |
| | | btn.setTitleColor(Config.ThemeColor, for: .normal) |
| | | btn.jq_borderColor = Config.ThemeColor |
| | | btn.backgroundColor = .white |
| | | btn.jq_borderWidth = 1 |
| | | btn.jq_cornerRadius = 4 |
| | | return btn |
| | | }() |
| | | private lazy var btn_beAgain:UIButton = { |
| | | let btn = UIButton(type: .custom) |
| | | btn.setTitle("重新开始", for: .normal) |
| | | btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) |
| | | btn.setTitleColor(Config.ThemeColor, for: .normal) |
| | | btn.jq_borderColor = Config.ThemeColor |
| | | btn.backgroundColor = .white |
| | | btn.jq_borderWidth = 1 |
| | | btn.jq_cornerRadius = 4 |
| | | return btn |
| | | }() |
| | | |
| | | private lazy var btn_exit:UIButton = { |
| | | let btn = UIButton(type: .custom) |
| | | btn.setTitle("退出", for: .normal) |
| | | btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) |
| | | btn.setTitleColor(.white, for: .normal) |
| | | btn.backgroundColor = Config.ThemeColor |
| | | btn.jq_cornerRadius = 4 |
| | | return btn |
| | | }() |
| | | private lazy var btn_continue:UIButton = { |
| | | let btn = UIButton(type: .custom) |
| | | btn.setTitle("继续答题", for: .normal) |
| | | btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) |
| | | btn.setTitleColor(Config.ThemeColor, for: .normal) |
| | | btn.jq_borderColor = Config.ThemeColor |
| | | btn.backgroundColor = .white |
| | | btn.jq_borderWidth = 1 |
| | | btn.jq_cornerRadius = 4 |
| | | return btn |
| | | }() |
| | | |
| | | private lazy var pageVC:FFPageViewController = { |
| | | let vc = FFPageViewController() |
| | | vc.scrollview.isScrollEnabled = false |
| | | return vc |
| | | }() |
| | | private lazy var btn_forward_mini:UIButton = { |
| | | let btn = UIButton(type: .custom) |
| | | btn.setTitle("上一小题", for: .normal) |
| | | btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) |
| | | btn.setTitleColor(Config.ThemeColor, for: .normal) |
| | | btn.jq_borderColor = Config.ThemeColor |
| | | btn.backgroundColor = .white |
| | | btn.isHidden = true |
| | | btn.jq_borderWidth = 1 |
| | | btn.jq_cornerRadius = 4 |
| | | return btn |
| | | }() |
| | | |
| | | private var timer:Timer! |
| | | private lazy var btn_next:UIButton = { |
| | | let btn = UIButton(type: .custom) |
| | | btn.setTitle("下一题", for: .normal) |
| | | btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) |
| | | btn.setTitleColor(Config.ThemeColor, for: .normal) |
| | | btn.jq_borderColor = Config.ThemeColor |
| | | btn.backgroundColor = .white |
| | | btn.jq_borderWidth = 1 |
| | | btn.jq_cornerRadius = 4 |
| | | return btn |
| | | }() |
| | | |
| | | private lazy var btn_exit:UIButton = { |
| | | let btn = UIButton(type: .custom) |
| | | btn.setTitle("退出", for: .normal) |
| | | btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) |
| | | btn.setTitleColor(.white, for: .normal) |
| | | btn.backgroundColor = Config.ThemeColor |
| | | btn.jq_cornerRadius = 4 |
| | | return btn |
| | | }() |
| | | |
| | | init(listenType:ListenType,quarter:Int? = nil,week:Int? = nil,day:Int? = nil) { |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.viewModel.listenType.accept(listenType) |
| | | self.viewModel.week.accept(week) |
| | | self.viewModel.day.accept(day) |
| | | self.viewModel.quarter.accept(quarter) |
| | | private lazy var pageVC:FFPageViewController = { |
| | | let vc = FFPageViewController() |
| | | vc.scrollview.isScrollEnabled = false |
| | | return vc |
| | | }() |
| | | |
| | | if listenType == .game1 || listenType == .game2{ |
| | | self.viewModel.maxPage.accept(1) |
| | | } |
| | | } |
| | | private var timer:Timer! |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | sceneDelegate?.suspendTimer() |
| | | self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true |
| | | } |
| | | init(listenType:ListenType,quarter:Int? = nil,week:Int? = nil,day:Int? = nil) { |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.viewModel.listenType.accept(listenType) |
| | | self.viewModel.week.accept(week) |
| | | self.viewModel.day.accept(day) |
| | | self.viewModel.quarter.accept(quarter) |
| | | |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | if listenType == .game1 || listenType == .game2{ |
| | | self.viewModel.maxPage.accept(1) |
| | | } |
| | | } |
| | | |
| | | self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | |
| | | yy_popBlock = {[weak self] in |
| | | self?.quitAction(isPop: true) |
| | | } |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | sceneDelegate?.suspendTimer() |
| | | self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true |
| | | } |
| | | |
| | | btn_exit.addTarget(self, action: #selector(quitAction), for: .touchUpInside) |
| | | btn_forward.addTarget(self, action: #selector(beforeAction), for: .touchUpInside) |
| | | btn_forward_mini.addTarget(self, action: #selector(beforeAction_mini), for: .touchUpInside) |
| | | btn_next.addTarget(self, action: #selector(nextAction), for: .touchUpInside) |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | |
| | | setPages() |
| | | pageVC.reloadData() |
| | | self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false |
| | | |
| | | timer = Timer(fire: .distantPast, interval: 1.0, repeats: true, block: {[weak self] _ in |
| | | self?.viewModel.times += 1 |
| | | }) |
| | | yy_popBlock = {[weak self] in |
| | | self?.quitAction(isPop: true) |
| | | } |
| | | |
| | | timer.fire() |
| | | RunLoop.current.add(timer, forMode: .common) |
| | | btn_exit.addTarget(self, action: #selector(quitAction), for: .touchUpInside) |
| | | btn_forward.addTarget(self, action: #selector(beforeAction), for: .touchUpInside) |
| | | btn_forward_mini.addTarget(self, action: #selector(beforeAction_mini), for: .touchUpInside) |
| | | btn_next.addTarget(self, action: #selector(nextAction), for: .touchUpInside) |
| | | btn_beAgain.addTarget(self, action: #selector(beAgaionAction), for: .touchUpInside) |
| | | |
| | | setPages() |
| | | pageVC.reloadData() |
| | | |
| | | if let teamSchedule = teamScheduleModel{ |
| | | viewModel.correctNum = viewModel.correctNum + teamSchedule.correctNumber |
| | | viewModel.errorNum = teamSchedule.answerNumber - teamSchedule.correctNumber |
| | | maxPage = teamSchedule.schedule |
| | | let ids = (data as! ListenNewModel).data!.id.components(separatedBy: ",") |
| | | switch viewModel.listenType.value { |
| | | case .lesson1: |
| | | let nextPage = floor(Double(maxPage) / 5.0) |
| | | pageVC.scroll(toPage: Int(nextPage), animation: false) |
| | | viewModel.answerCount.accept(maxPage) |
| | | setPages() |
| | | timer = Timer(fire: .distantPast, interval: 1.0, repeats: true, block: {[weak self] _ in |
| | | self?.viewModel.times += 1 |
| | | }) |
| | | |
| | | case .lesson2: |
| | | let maxCount = (data as! ListenNewModel).subjectList.count |
| | | let page = min((maxPage - 1),maxCount) |
| | | if pageVC.currentPage != page{ |
| | | viewModel.currentPage.accept(page) |
| | | pageVC.scroll(toPage: page, animation: false) |
| | | setPages() |
| | | } |
| | | timer.fire() |
| | | RunLoop.current.add(timer, forMode: .common) |
| | | setPages() |
| | | |
| | | case .lesson4: |
| | | let maxCount = (data as! ListenNewModel).subjectList.count |
| | | let page = min((maxPage - 1),maxCount) |
| | | if pageVC.currentPage != page{ |
| | | viewModel.currentPage.accept(page) |
| | | pageVC.scroll(toPage: page, animation: false) |
| | | setPages() |
| | | } |
| | | if let teamSchedule = teamScheduleModel{ |
| | | viewModel.correctNum = viewModel.correctNum + teamSchedule.correctNumber |
| | | viewModel.errorNum = teamSchedule.answerNumber - teamSchedule.correctNumber |
| | | maxPage = teamSchedule.schedule |
| | | |
| | | switch viewModel.listenType.value{ |
| | | case .lesson1: |
| | | |
| | | case .lesson3,.lesson5: |
| | | let maxCount = (data as! ListenNewModel).subjectList.count |
| | | let page = min((maxPage - 1),maxCount) |
| | | if pageVC.currentPage != page{ |
| | | viewModel.currentPage.accept(page) |
| | | pageVC.scroll(toPage: page, animation: false) |
| | | setPages() |
| | | } |
| | | if let m = data as? ListenNewModel{ |
| | | self.pages = Array<ListenSubCardModel>.splitArray(m.list, subArraySize: 4) |
| | | let c = m.list.count / 4 |
| | | let totalW = 24 * c + 2 * (c - 1) |
| | | collection_card.snp.updateConstraints { make in |
| | | make.width.equalTo(totalW) |
| | | } |
| | | } |
| | | case .lesson2,.lesson3,.lesson4,.lesson5: |
| | | if let m = data as? ListenNewModel{ |
| | | self.pages = Array<ListenSubCardModel>.splitArray(m.list, subArraySize: 1) |
| | | let totalW = 24 * m.list.count + 2 * (m.list.count - 1) |
| | | collection_card.snp.updateConstraints { make in |
| | | make.width.equalTo(totalW) |
| | | } |
| | | } |
| | | default:break |
| | | } |
| | | |
| | | switch viewModel.listenType.value { |
| | | case .lesson1: |
| | | collection_card.reloadData() |
| | | let nextPage = floor(Double(maxPage) / 5.0) |
| | | pageVC.scroll(toPage: Int(nextPage), animation: false) |
| | | viewModel.answerCount.accept(maxPage) |
| | | setPages() |
| | | |
| | | default:break |
| | | } |
| | | } |
| | | } |
| | | case .lesson2: |
| | | collection_card.reloadData() |
| | | let maxCount = (data as! ListenNewModel).subjectList.count |
| | | let page = min((maxPage - 1),maxCount) |
| | | if pageVC.currentPage != page{ |
| | | viewModel.currentPage.accept(page) |
| | | pageVC.scroll(toPage: page, animation: false) |
| | | setPages() |
| | | } |
| | | |
| | | override func setUI() { |
| | | super.setUI() |
| | | craeteFootFuncView() |
| | | case .lesson4: |
| | | collection_card.reloadData() |
| | | let maxCount = (data as! ListenNewModel).subjectList.count |
| | | let page = min((maxPage - 1),maxCount) |
| | | if pageVC.currentPage != page{ |
| | | viewModel.currentPage.accept(page) |
| | | pageVC.scroll(toPage: page, animation: false) |
| | | setPages() |
| | | } |
| | | |
| | | pageVC.delegate = self |
| | | view.addSubview(pageVC.view) |
| | | pageVC.view.snp.makeConstraints { make in |
| | | make.left.right.equalToSuperview() |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top) |
| | | if self.viewModel.listenType.value == .lesson3 || self.viewModel.listenType.value == .lesson4{ |
| | | make.bottom.equalTo(self.label_pageNum.snp.top).offset(-18) |
| | | }else{ |
| | | make.bottom.equalTo(self.label_pageNum.snp.top).offset(-32) |
| | | } |
| | | } |
| | | } |
| | | |
| | | case .lesson3,.lesson5: |
| | | collection_card.reloadData() |
| | | let maxCount = (data as! ListenNewModel).subjectList.count |
| | | let page = min((maxPage - 1),maxCount) |
| | | if pageVC.currentPage != page{ |
| | | viewModel.currentPage.accept(page) |
| | | pageVC.scroll(toPage: page, animation: false) |
| | | setPages() |
| | | } |
| | | |
| | | private func showGameLevel(canLevel:Int){ |
| | | ChooseLevelView.show(canLevel: canLevel) {[weak self] level in |
| | | guard let weakSelf = self else { return } |
| | | weakSelf.viewModel.gameLevel.accept(level) |
| | | Services.gameHearing(difficulty: level, quarter: weakSelf.viewModel.quarter.value!, week: weakSelf.viewModel.week.value!).subscribe(onNext: {result in |
| | | GameBeginTipView.show { |
| | | if let data = result.data{ |
| | | weakSelf.data = data |
| | | (weakSelf.data as! Listen1Model).data?.playNow = true |
| | | weakSelf.pageVC.reloadData() |
| | | } |
| | | } |
| | | },onError: {[weak self] _ in |
| | | self?.navigationController?.popViewController(animated: true) |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | } cancelClouse: { [weak self] in |
| | | self?.navigationController?.popViewController(animated: true) |
| | | } |
| | | } |
| | | |
| | | private func craeteFootFuncView(){ |
| | | default:break |
| | | } |
| | | } |
| | | } |
| | | |
| | | override func setUI() { |
| | | super.setUI() |
| | | |
| | | btn_forward.snp.makeConstraints { make in |
| | | make.height.equalTo(40) |
| | | make.width.equalTo(124) |
| | | } |
| | | switch viewModel.listenType.value{ |
| | | case .lesson1,.lesson2,.lesson3,.lesson4,.lesson5: |
| | | collection_card.delegate = self |
| | | collection_card.dataSource = self |
| | | collection_card.register(CardItemCCell.self, forCellWithReuseIdentifier: "_CardItemCCell") |
| | | default:break |
| | | } |
| | | |
| | | btn_exit.snp.makeConstraints { make in |
| | | make.height.equalTo(40) |
| | | make.width.equalTo(124) |
| | | } |
| | | craeteFootFuncView() |
| | | |
| | | let stackView = UIStackView(arrangedSubviews: [btn_forward,label_pageNum,btn_exit]) |
| | | if viewModel.listenType.value == .story2{ |
| | | btn_next.snp.makeConstraints { make in |
| | | make.height.equalTo(40) |
| | | make.width.equalTo(124) |
| | | } |
| | | stackView.insertArrangedSubview(btn_next, at: 2) |
| | | } |
| | | pageVC.delegate = self |
| | | view.addSubview(pageVC.view) |
| | | pageVC.view.snp.makeConstraints { make in |
| | | make.left.right.equalToSuperview() |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top) |
| | | if self.viewModel.listenType.value == .lesson3 || self.viewModel.listenType.value == .lesson4{ |
| | | make.bottom.equalTo(self.label_pageNum.snp.top).offset(-52) |
| | | }else{ |
| | | make.bottom.equalTo(self.label_pageNum.snp.top).offset(-52) |
| | | } |
| | | } |
| | | } |
| | | |
| | | stackView.spacing = 22 |
| | | view.addSubview(stackView) |
| | | stackView.snp.makeConstraints { make in |
| | | make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom).offset(-22) |
| | | make.centerX.equalToSuperview() |
| | | make.height.equalTo(40) |
| | | } |
| | | } |
| | | |
| | | private func showGameLevel(canLevel:Int){ |
| | | ChooseLevelView.show(canLevel: canLevel) {[weak self] level in |
| | | guard let weakSelf = self else { return } |
| | | weakSelf.viewModel.gameLevel.accept(level) |
| | | Services.gameHearing(difficulty: level, quarter: weakSelf.viewModel.quarter.value!, week: weakSelf.viewModel.week.value!).subscribe(onNext: {result in |
| | | GameBeginTipView.show { |
| | | if let data = result.data{ |
| | | weakSelf.data = data |
| | | (weakSelf.data as! Listen1Model).data?.playNow = true |
| | | weakSelf.pageVC.reloadData() |
| | | } |
| | | } |
| | | },onError: {[weak self] _ in |
| | | self?.navigationController?.popViewController(animated: true) |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | } cancelClouse: { [weak self] in |
| | | self?.navigationController?.popViewController(animated: true) |
| | | } |
| | | } |
| | | |
| | | private func craeteFootFuncView(){ |
| | | |
| | | view.addSubview(collection_card) |
| | | |
| | | btn_forward.snp.makeConstraints { make in |
| | | make.height.equalTo(40) |
| | | make.width.equalTo(124) |
| | | } |
| | | |
| | | btn_beAgain.snp.makeConstraints { make in |
| | | make.height.equalTo(40) |
| | | make.width.equalTo(124) |
| | | } |
| | | |
| | | btn_continue.snp.makeConstraints { make in |
| | | make.height.equalTo(40) |
| | | make.width.equalTo(124) |
| | | } |
| | | |
| | | |
| | | btn_exit.snp.makeConstraints { make in |
| | | make.height.equalTo(40) |
| | | make.width.equalTo(124) |
| | | } |
| | | |
| | | |
| | | let stackView = UIStackView(arrangedSubviews: [btn_beAgain,label_pageNum,btn_exit]) |
| | | if viewModel.listenType.value == .story2{ |
| | | btn_next.snp.makeConstraints { make in |
| | | make.height.equalTo(40) |
| | | make.width.equalTo(124) |
| | | } |
| | | stackView.insertArrangedSubview(btn_next, at: 2) |
| | | } |
| | | |
| | | stackView.spacing = 22 |
| | | view.addSubview(stackView) |
| | | stackView.snp.makeConstraints { make in |
| | | make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom).offset(-5) |
| | | make.centerX.equalToSuperview() |
| | | make.height.equalTo(40) |
| | | } |
| | | |
| | | switch viewModel.listenType.value{ |
| | | case .lesson1,.lesson2,.lesson3,.lesson4,.lesson5: |
| | | collection_card.snp.makeConstraints { make in |
| | | make.centerX.equalToSuperview() |
| | | make.width.equalTo(10) |
| | | make.bottom.equalTo(stackView.snp.top).offset(-10) |
| | | make.height.equalTo(24) |
| | | } |
| | | default:break |
| | | } |
| | | } |
| | | |
| | | override func setRx() { |
| | | NotificationCenter.default.rx.notification(NextLession_Noti).take(until: self.rx.deallocated).subscribe(onNext: {[weak self] noti in |
| | | guard let weakSelf = self else { return } |
| | | let nextPage = weakSelf.viewModel.currentPage.value + 1 |
| | | var asComplete:Bool = false |
| | | switch weakSelf.viewModel.listenType.value { |
| | | case .lesson1,.lesson2,.lesson3,.lesson4,.lesson5:asComplete = nextPage >= (weakSelf.data as! ListenNewModel).subjectList.count |
| | | case .game1,.game2:asComplete = true |
| | | case .story1,.story2: asComplete = nextPage >= (weakSelf.data as! Listen1Model).storyList.count |
| | | } |
| | | |
| | | if asComplete{ |
| | | switch weakSelf.viewModel.listenType.value { |
| | | case .lesson1,.lesson2,.lesson3,.lesson4,.lesson5: |
| | | weakSelf.studyComplete() |
| | | case .game1,.game2: |
| | | weakSelf.notiObject = noti.object as? Dictionary<String,Any> |
| | | weakSelf.timer.invalidate() |
| | | |
| | | if let isComplete = weakSelf.notiObject?["complete"] as? Bool{ |
| | | if isComplete{ |
| | | weakSelf.btn_exit.setTitle("提交", for: .normal) |
| | | }else{ |
| | | weakSelf.gamesComplete(gameId: weakSelf.notiObject!["gameId"] as! Int,integral: weakSelf.notiObject!["gameIntegral"] as! Int) |
| | | } |
| | | } |
| | | case .story1,.story2: |
| | | if let dict = noti.object as? Dictionary<String,Any>{ |
| | | let type = weakSelf.viewModel.listenType.value == .story1 ? 1:2 |
| | | let accracy = floor(Double(weakSelf.viewModel.correctNum) / Double(weakSelf.viewModel.correctNum + weakSelf.viewModel.errorNum) * 100).int |
| | | weakSelf.storyComplete(storyId: dict["storyId"] as! Int, accuracy: accracy, studyTime: weakSelf.viewModel.times, type: type, integral: dict["storyIntegral"] as! Int) |
| | | } |
| | | } |
| | | return |
| | | } |
| | | |
| | | if weakSelf.viewModel.listenType.value == .story2{ |
| | | weakSelf.btn_next.isHidden = (nextPage + 1) == weakSelf.viewModel.maxPage.value |
| | | if weakSelf.btn_next.isHidden{ |
| | | weakSelf.btn_exit.setTitle("完成", for: .normal) |
| | | } |
| | | } |
| | | |
| | | weakSelf.listenFightLine = .next |
| | | weakSelf.pageVC.scroll(toPage: nextPage, animation: true) |
| | | weakSelf.viewModel.currentPage.accept(nextPage) |
| | | }).disposed(by: disposeBag) |
| | | |
| | | viewModel.currentPage.subscribe(onNext: {[weak self]currentPage in |
| | | guard let weakSelf = self else { return } |
| | | weakSelf.btn_forward.isHidden = currentPage <= 0 |
| | | weakSelf.setPages() |
| | | }).disposed(by: disposeBag) |
| | | |
| | | viewModel.answerCount.subscribe(onNext: {[weak self] count in |
| | | self?.setPages() |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | private func setPages(){ |
| | | switch viewModel.listenType.value{ |
| | | case .lesson1: |
| | | let m = data as! ListenNewModel |
| | | label_pageNum.text = "已完成:\(viewModel.currentPage.value + 1)/\(m.subjectList.flatMap({$0}).count / 4)" |
| | | let correctNum = m.list.filter({$0.status == 2}).count //正确 |
| | | // if correctNum > 0 { |
| | | let ratio = Double(correctNum) / Double(m.list.count) * 100.0 |
| | | let ratioStr = ratio.jq_formatFloat |
| | | |
| | | label_pageNum.text = "已完成:\(viewModel.currentPage.value + 1)/\(m.subjectList.flatMap({$0}).count / 4)\n正确率:\(ratioStr)%" |
| | | // } |
| | | |
| | | maxPage = viewModel.answerCount.value |
| | | btn_forward.isHidden = viewModel.answerCount.value == 1 |
| | | case .lesson3: |
| | | let m = data as! ListenNewModel |
| | | label_pageNum.text = "已完成:\(viewModel.currentPage.value + 1)/\(m.subjectList.count / 6)" |
| | | btn_forward.isHidden = viewModel.currentPage.value == 0 |
| | | let page = viewModel.currentPage.value + 1 |
| | | maxPage = page |
| | | |
| | | override func setRx() { |
| | | NotificationCenter.default.rx.notification(NextLession_Noti).take(until: self.rx.deallocated).subscribe(onNext: {[weak self] noti in |
| | | guard let weakSelf = self else { return } |
| | | let nextPage = weakSelf.viewModel.currentPage.value + 1 |
| | | var asComplete:Bool = false |
| | | switch weakSelf.viewModel.listenType.value { |
| | | case .lesson1,.lesson2,.lesson3,.lesson4,.lesson5:asComplete = nextPage >= (weakSelf.data as! ListenNewModel).subjectList.count |
| | | case .game1,.game2:asComplete = true |
| | | case .story1,.story2: asComplete = nextPage >= (weakSelf.data as! Listen1Model).storyList.count |
| | | } |
| | | let correctNum = m.list.filter({$0.status == 2}).count //正确 |
| | | let ratio = Double(correctNum) / Double(m.list.count) * 100.0 |
| | | let ratioStr = ratio.jq_formatFloat |
| | | label_pageNum.text = "已完成:\(viewModel.answerCount.value)/\(m.subjectList.flatMap({$0}).count)\n正确率:\(ratioStr)%" |
| | | |
| | | if asComplete{ |
| | | switch weakSelf.viewModel.listenType.value { |
| | | case .lesson1,.lesson2,.lesson3,.lesson4,.lesson5: |
| | | weakSelf.studyComplete() |
| | | case .game1,.game2: |
| | | weakSelf.notiObject = noti.object as? Dictionary<String,Any> |
| | | weakSelf.timer.invalidate() |
| | | case .lesson2,.lesson5: |
| | | let m = data as! ListenNewModel |
| | | label_pageNum.text = "已完成:\(viewModel.currentPage.value + 1)/\(m.subjectList.count)" |
| | | btn_forward.isHidden = viewModel.currentPage.value == 0 |
| | | let page = viewModel.currentPage.value + 1 |
| | | maxPage = page |
| | | |
| | | if let isComplete = weakSelf.notiObject?["complete"] as? Bool{ |
| | | if isComplete{ |
| | | weakSelf.btn_exit.setTitle("提交", for: .normal) |
| | | }else{ |
| | | weakSelf.gamesComplete(gameId: weakSelf.notiObject!["gameId"] as! Int,integral: weakSelf.notiObject!["gameIntegral"] as! Int) |
| | | } |
| | | } |
| | | case .story1,.story2: |
| | | if let dict = noti.object as? Dictionary<String,Any>{ |
| | | let type = weakSelf.viewModel.listenType.value == .story1 ? 1:2 |
| | | let accracy = floor(Double(weakSelf.viewModel.correctNum) / Double(weakSelf.viewModel.correctNum + weakSelf.viewModel.errorNum) * 100).int |
| | | weakSelf.storyComplete(storyId: dict["storyId"] as! Int, accuracy: accracy, studyTime: weakSelf.viewModel.times, type: type, integral: dict["storyIntegral"] as! Int) |
| | | } |
| | | } |
| | | return |
| | | } |
| | | let correctNum = m.list.filter({$0.status == 2}).count //正确 |
| | | let ratio = Double(correctNum) / Double(m.list.count) * 100.0 |
| | | let ratioStr = ratio.jq_formatFloat |
| | | label_pageNum.text = "已完成:\(viewModel.answerCount.value)/\(m.subjectList.flatMap({$0}).count)\n正确率:\(ratioStr)%" |
| | | |
| | | if weakSelf.viewModel.listenType.value == .story2{ |
| | | weakSelf.btn_next.isHidden = (nextPage + 1) == weakSelf.viewModel.maxPage.value |
| | | if weakSelf.btn_next.isHidden{ |
| | | weakSelf.btn_exit.setTitle("完成", for: .normal) |
| | | } |
| | | } |
| | | case .lesson4: |
| | | let m = data as! ListenNewModel |
| | | //两题为一组:需要/2 |
| | | label_pageNum.text = "已完成:\(viewModel.currentPage.value + 1)/\(m.subjectList.count)" |
| | | let page = viewModel.currentPage.value + 1 |
| | | // maxPage = max(page,maxPage) |
| | | maxPage = page |
| | | |
| | | weakSelf.listenFightLine = .next |
| | | weakSelf.pageVC.scroll(toPage: nextPage, animation: true) |
| | | weakSelf.viewModel.currentPage.accept(nextPage) |
| | | }).disposed(by: disposeBag) |
| | | let correctNum = m.list.filter({$0.status == 2}).count //正确 |
| | | let ratio = Double(correctNum) / Double(m.list.count) * 100.0 |
| | | let ratioStr = ratio.jq_formatFloat |
| | | label_pageNum.text = "已完成:\(viewModel.answerCount.value)/\(m.subjectList.flatMap({$0}).count)\n正确率:\(ratioStr)%" |
| | | case .game1,.game2: |
| | | btn_forward.isHidden = true |
| | | label_pageNum.isHidden = true |
| | | |
| | | viewModel.currentPage.subscribe(onNext: {[weak self]currentPage in |
| | | guard let weakSelf = self else { return } |
| | | weakSelf.btn_forward.isHidden = currentPage <= 0 |
| | | weakSelf.setPages() |
| | | }).disposed(by: disposeBag) |
| | | if viewModel.listenType.value == .game1{ |
| | | showGameLevel(canLevel: studyScheduleModel?.gameDifficulty ?? 0) |
| | | } |
| | | case .story1: |
| | | if viewModel.listenType.value == .story2{ |
| | | btn_next.isHidden = (viewModel.currentPage.value + 1) == viewModel.maxPage.value |
| | | if btn_next.isHidden{ |
| | | btn_exit.setTitle("完成", for: .normal) |
| | | } |
| | | } |
| | | fallthrough |
| | | case .story2: |
| | | let count = (data as! Listen1Model).storyList.count |
| | | viewModel.maxPage.accept(count) |
| | | label_pageNum.text = "已完成:\(viewModel.currentPage.value + 1)/\(count)" |
| | | } |
| | | } |
| | | |
| | | viewModel.answerCount.subscribe(onNext: {[weak self] count in |
| | | self?.setPages() |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | private func setPages(){ |
| | | switch viewModel.listenType.value{ |
| | | case .lesson1: |
| | | label_pageNum.text = "已完成:\(viewModel.answerCount.value)/\((data as! ListenNewModel).subjectList.flatMap({$0}).count)" |
| | | // maxPage = max(viewModel.answerCount.value,maxPage) |
| | | maxPage = viewModel.answerCount.value |
| | | btn_forward.isHidden = viewModel.answerCount.value == 1 |
| | | case .lesson2,.lesson3,.lesson5: |
| | | label_pageNum.text = "已完成:\(viewModel.currentPage.value + 1)/\((data as! ListenNewModel).subjectList.count)" |
| | | btn_forward.isHidden = viewModel.currentPage.value == 0 |
| | | let page = viewModel.currentPage.value + 1 |
| | | // maxPage = max(page,maxPage) |
| | | maxPage = page |
| | | case .lesson4: |
| | | //两题为一组:需要/2 |
| | | label_pageNum.text = "已完成:\(viewModel.currentPage.value + 1)/\((data as! ListenNewModel).subjectList.count)" |
| | | let page = viewModel.currentPage.value + 1 |
| | | // maxPage = max(page,maxPage) |
| | | maxPage = page |
| | | case .game1,.game2: |
| | | btn_forward.isHidden = true |
| | | label_pageNum.isHidden = true |
| | | /// 学习类完成 |
| | | /// - Parameter ignorePush: 是否忽略跳转(未完成答题 :true) |
| | | private func studyComplete(){ |
| | | let ids:String = viewModel.answerItems_1.keys.sorted().joined(separator: ",") |
| | | |
| | | if viewModel.listenType.value == .game1{ |
| | | showGameLevel(canLevel: studyScheduleModel?.gameDifficulty ?? 0) |
| | | } |
| | | case .story1: |
| | | if viewModel.listenType.value == .story2{ |
| | | btn_next.isHidden = (viewModel.currentPage.value + 1) == viewModel.maxPage.value |
| | | if btn_next.isHidden{ |
| | | btn_exit.setTitle("完成", for: .normal) |
| | | } |
| | | } |
| | | fallthrough |
| | | case .story2: |
| | | let count = (data as! Listen1Model).storyList.count |
| | | viewModel.maxPage.accept(count) |
| | | label_pageNum.text = "已完成:\(viewModel.currentPage.value + 1)/\(count)" |
| | | } |
| | | } |
| | | //正确率 |
| | | let accracy = floor(Double(viewModel.correctNum) / Double(viewModel.correctNum + viewModel.errorNum) * 100).int |
| | | |
| | | Services.completeLearing(type: viewModel.listenType.value.rawValue, studyTime: viewModel.times, studyIds: ids, quarter: viewModel.quarter.value!, week: viewModel.week.value!, day: viewModel.day.value!, accracy: accracy).subscribe(onNext: {data in |
| | | NotificationCenter.default.post(name: Refresh_ListenSchedule_Noti, object: nil) |
| | | NotificationCenter.default.post(name: StudyCompleteCoinUpdate_Noti, object: data.data ?? 0) |
| | | }).disposed(by: disposeBag) |
| | | |
| | | /// 学习类完成 |
| | | /// - Parameter ignorePush: 是否忽略跳转(未完成答题 :true) |
| | | private func studyComplete(){ |
| | | let ids:String = viewModel.answerItems_1.keys.sorted().joined(separator: ",") |
| | | timer.invalidate() |
| | | |
| | | //正确率 |
| | | let accracy = floor(Double(viewModel.correctNum) / Double(viewModel.correctNum + viewModel.errorNum) * 100).int |
| | | let vc = HomeStudyCompleteVC(viewModel: viewModel,studyScheduleModel: studyScheduleModel!) |
| | | vc.title = viewModel.listenType.value.rawTitle |
| | | vc.viewModel = viewModel |
| | | push(vc: vc) |
| | | } |
| | | |
| | | Services.completeLearing(type: viewModel.listenType.value.rawValue, studyTime: viewModel.times, studyIds: ids, quarter: viewModel.quarter.value!, week: viewModel.week.value!, day: viewModel.day.value!, accracy: accracy).subscribe(onNext: {data in |
| | | NotificationCenter.default.post(name: Refresh_ListenSchedule_Noti, object: nil) |
| | | NotificationCenter.default.post(name: StudyCompleteCoinUpdate_Noti, object: data.data ?? 0) |
| | | }).disposed(by: disposeBag) |
| | | //游戏类完成 |
| | | private func gamesComplete(gameId:Int,integral:Int){ |
| | | |
| | | timer.invalidate() |
| | | var name = "" |
| | | var accuracy:Int = 0 |
| | | var totalNum:Double = 0 |
| | | |
| | | let vc = HomeStudyCompleteVC(viewModel: viewModel,studyScheduleModel: studyScheduleModel!) |
| | | vc.title = viewModel.listenType.value.rawTitle |
| | | vc.viewModel = viewModel |
| | | push(vc: vc) |
| | | } |
| | | if viewModel.listenType.value == .game1{ |
| | | name = "超级听力" |
| | | totalNum = Double(viewModel.correctNum + viewModel.errorNum) |
| | | if totalNum > 0{ |
| | | accuracy = floor(Double(viewModel.correctNum) / totalNum * 100).int |
| | | } |
| | | }else{ |
| | | name = "超级记忆" |
| | | let v = viewModel.answerItems.first?.value as! Listen1Model |
| | | //11887:完成答题页,总题目、错误题目 数量计算逻辑错误(只要是没有答对的题目就算错误题目,不管是否答,相当于错误题目就是总题目减去正确题目) |
| | | totalNum = Double(v.photoList.count) |
| | | if totalNum > 0 && viewModel.correctNum > 0 && viewModel.errorNum > 0{ |
| | | accuracy = floor(Double(viewModel.correctNum) / Double(totalNum) * 100).int |
| | | } |
| | | viewModel.errorNum = Int(totalNum) - viewModel.correctNum |
| | | } |
| | | |
| | | //游戏类完成 |
| | | private func gamesComplete(gameId:Int,integral:Int){ |
| | | Services.completeGames(gameId: gameId, gameName: name, difficulty: viewModel.gameLevel.value, accuracy: accuracy, useTime: viewModel.times).subscribe(onNext: {data in |
| | | NotificationCenter.default.post(name: Refresh_ListenSchedule_Noti, object: nil) |
| | | NotificationCenter.default.post(name: StudyCompleteCoinUpdate_Noti, object: data.data ?? 0) |
| | | }).disposed(by: disposeBag) |
| | | |
| | | var name = "" |
| | | var accuracy:Int = 0 |
| | | var totalNum:Double = 0 |
| | | timer.invalidate() |
| | | |
| | | if viewModel.listenType.value == .game1{ |
| | | name = "超级听力" |
| | | totalNum = Double(viewModel.correctNum + viewModel.errorNum) |
| | | if totalNum > 0{ |
| | | accuracy = floor(Double(viewModel.correctNum) / totalNum * 100).int |
| | | } |
| | | }else{ |
| | | name = "超级记忆" |
| | | let v = viewModel.answerItems.first?.value as! Listen1Model |
| | | //11887:完成答题页,总题目、错误题目 数量计算逻辑错误(只要是没有答对的题目就算错误题目,不管是否答,相当于错误题目就是总题目减去正确题目) |
| | | totalNum = Double(v.photoList.count) |
| | | if totalNum > 0 && viewModel.correctNum > 0 && viewModel.errorNum > 0{ |
| | | accuracy = floor(Double(viewModel.correctNum) / Double(totalNum) * 100).int |
| | | } |
| | | viewModel.errorNum = Int(totalNum) - viewModel.correctNum |
| | | } |
| | | let vc = HomeStudyCompleteVC(totalNum:totalNum.int,viewModel: viewModel,studyScheduleModel: studyScheduleModel!) |
| | | vc.title = viewModel.listenType.value.rawTitle |
| | | vc.viewModel = viewModel |
| | | push(vc: vc) |
| | | } |
| | | |
| | | Services.completeGames(gameId: gameId, gameName: name, difficulty: viewModel.gameLevel.value, accuracy: accuracy, useTime: viewModel.times).subscribe(onNext: {data in |
| | | NotificationCenter.default.post(name: Refresh_ListenSchedule_Noti, object: nil) |
| | | NotificationCenter.default.post(name: StudyCompleteCoinUpdate_Noti, object: data.data ?? 0) |
| | | }).disposed(by: disposeBag) |
| | | private func storyComplete(storyId:Int,accuracy:Int,studyTime:Int,type:Int,integral:Int){ |
| | | timer.invalidate() |
| | | Services.completeStory(storyId: storyId, accuracy: accuracy, studyTime: studyTime, type: type).subscribe(onNext: {data in |
| | | NotificationCenter.default.post(name: Refresh_ListenSchedule_Noti, object: nil) |
| | | NotificationCenter.default.post(name: StudyCompleteCoinUpdate_Noti, object: data.data ?? 0) |
| | | }).disposed(by: disposeBag) |
| | | |
| | | timer.invalidate() |
| | | let vc = HomeStudyCompleteVC(viewModel: viewModel,studyScheduleModel: studyScheduleModel!) |
| | | vc.title = viewModel.listenType.value.rawTitle |
| | | vc.viewModel = viewModel |
| | | push(vc: vc) |
| | | } |
| | | |
| | | let vc = HomeStudyCompleteVC(totalNum:totalNum.int,viewModel: viewModel,studyScheduleModel: studyScheduleModel!) |
| | | vc.title = viewModel.listenType.value.rawTitle |
| | | vc.viewModel = viewModel |
| | | push(vc: vc) |
| | | } |
| | | deinit{ |
| | | timer.invalidate() |
| | | } |
| | | |
| | | private func storyComplete(storyId:Int,accuracy:Int,studyTime:Int,type:Int,integral:Int){ |
| | | timer.invalidate() |
| | | Services.completeStory(storyId: storyId, accuracy: accuracy, studyTime: studyTime, type: type).subscribe(onNext: {data in |
| | | NotificationCenter.default.post(name: Refresh_ListenSchedule_Noti, object: nil) |
| | | NotificationCenter.default.post(name: StudyCompleteCoinUpdate_Noti, object: data.data ?? 0) |
| | | }).disposed(by: disposeBag) |
| | | @objc func quitAction(isPop:Bool = false){ |
| | | if btn_exit.titleLabel?.text == "完成"{ |
| | | if viewModel.listenType.value == .story2{ |
| | | |
| | | let vc = HomeStudyCompleteVC(viewModel: viewModel,studyScheduleModel: studyScheduleModel!) |
| | | vc.title = viewModel.listenType.value.rawTitle |
| | | vc.viewModel = viewModel |
| | | push(vc: vc) |
| | | } |
| | | if isPop{ |
| | | CommonAlertView.show(content: "未完成全部答题,确认退出吗?") {[weak self] () in |
| | | guard let weakSelf = self else { return } |
| | | for vc in weakSelf.navigationController?.viewControllers ?? []{ |
| | | if vc.isKind(of: HomeListenVC.self){ |
| | | weakSelf.navigationController?.popToViewController(vc, animated: true) |
| | | NotificationCenter.default.post(name: Refresh_ListenSchedule_Noti, object: nil) |
| | | break |
| | | } |
| | | } |
| | | } |
| | | return |
| | | } |
| | | |
| | | deinit{ |
| | | timer.invalidate() |
| | | } |
| | | guard (pageVC.currentController as! HomeListenStory_2_VC).isPlayEnd else { |
| | | alert(msg: "请听完");return |
| | | } |
| | | |
| | | @objc func quitAction(isPop:Bool = false){ |
| | | if btn_exit.titleLabel?.text == "完成"{ |
| | | if viewModel.listenType.value == .story2{ |
| | | let v = data as! Listen1Model |
| | | let accuracy = 100 |
| | | storyComplete(storyId: v.data!.id, accuracy: accuracy, studyTime: viewModel.times, type: viewModel.listenType.value == .story1 ? 1:2, integral: v.data!.integral) |
| | | } |
| | | }else if btn_exit.titleLabel?.text == "提交"{ |
| | | |
| | | if isPop{ |
| | | CommonAlertView.show(content: "未完成全部答题,确认退出吗?") {[weak self] () in |
| | | guard let weakSelf = self else { return } |
| | | for vc in weakSelf.navigationController?.viewControllers ?? []{ |
| | | if vc.isKind(of: HomeListenVC.self){ |
| | | weakSelf.navigationController?.popToViewController(vc, animated: true) |
| | | NotificationCenter.default.post(name: Refresh_ListenSchedule_Noti, object: nil) |
| | | break |
| | | } |
| | | } |
| | | } |
| | | return |
| | | } |
| | | if isPop{ |
| | | CommonAlertView.show(content: "未完成全部答题,确认退出吗?") {[weak self] () in |
| | | guard let weakSelf = self else { return } |
| | | for vc in weakSelf.navigationController?.viewControllers ?? []{ |
| | | if vc.isKind(of: HomeListenVC.self){ |
| | | weakSelf.navigationController?.popToViewController(vc, animated: true) |
| | | NotificationCenter.default.post(name: Refresh_ListenSchedule_Noti, object: nil) |
| | | break |
| | | } |
| | | } |
| | | } |
| | | return |
| | | } |
| | | |
| | | guard (pageVC.currentController as! HomeListenStory_2_VC).isPlayEnd else { |
| | | alert(msg: "请听完");return |
| | | } |
| | | if viewModel.listenType.value == .game1 || viewModel.listenType.value == .game2{ |
| | | if let dict = notiObject{ |
| | | gamesComplete(gameId: dict["gameId"] as! Int,integral: dict["gameIntegral"] as! Int) |
| | | } |
| | | } |
| | | } else{ |
| | | CommonAlertView.show(content: "未完成全部答题,确认退出吗?") {[weak self] () in |
| | | guard let weakSelf = self else { return } |
| | | |
| | | let v = data as! Listen1Model |
| | | let accuracy = 100 |
| | | storyComplete(storyId: v.data!.id, accuracy: accuracy, studyTime: viewModel.times, type: viewModel.listenType.value == .story1 ? 1:2, integral: v.data!.integral) |
| | | } |
| | | }else if btn_exit.titleLabel?.text == "提交"{ |
| | | let temIds = [String]() |
| | | let topicIds = [String]() |
| | | |
| | | if isPop{ |
| | | CommonAlertView.show(content: "未完成全部答题,确认退出吗?") {[weak self] () in |
| | | guard let weakSelf = self else { return } |
| | | for vc in weakSelf.navigationController?.viewControllers ?? []{ |
| | | if vc.isKind(of: HomeListenVC.self){ |
| | | weakSelf.navigationController?.popToViewController(vc, animated: true) |
| | | NotificationCenter.default.post(name: Refresh_ListenSchedule_Noti, object: nil) |
| | | break |
| | | } |
| | | } |
| | | } |
| | | return |
| | | } |
| | | |
| | | if viewModel.listenType.value == .game1 || viewModel.listenType.value == .game2{ |
| | | if let dict = notiObject{ |
| | | gamesComplete(gameId: dict["gameId"] as! Int,integral: dict["gameIntegral"] as! Int) |
| | | } |
| | | } |
| | | } else{ |
| | | CommonAlertView.show(content: "未完成全部答题,确认退出吗?") {[weak self] () in |
| | | guard let weakSelf = self else { return } |
| | | switch weakSelf.viewModel.listenType.value{ |
| | | case .lesson1,.lesson2,.lesson3,.lesson4,.lesson5: |
| | | let totalNum = weakSelf.viewModel.correctNum + weakSelf.viewModel.errorNum |
| | | Services.exitLearning(type:weakSelf.viewModel.listenType.value.rawValue,quarter: weakSelf.viewModel.quarter.value!,week: weakSelf.viewModel.week.value!, day: weakSelf.viewModel.day.value!, teamIds: temIds, topicIds: topicIds,answerNumber: totalNum,correctNumber:weakSelf.viewModel.correctNum,studyTime:weakSelf.viewModel.times,schedule: weakSelf.maxPage).subscribe(onNext: { data in |
| | | |
| | | let temIds = [String]() |
| | | let topicIds = [String]() |
| | | NotificationCenter.default.post(name: MeUserInfoUpdate_Noti, object: nil) |
| | | |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | case .game1,.game2,.story1,.story2: |
| | | Services.exitGameOrStory(studyTime: weakSelf.viewModel.times).subscribe(onNext: { _ in |
| | | |
| | | switch weakSelf.viewModel.listenType.value{ |
| | | case .lesson1,.lesson2,.lesson3,.lesson4,.lesson5: |
| | | let totalNum = weakSelf.viewModel.correctNum + weakSelf.viewModel.errorNum |
| | | Services.exitLearning(type:weakSelf.viewModel.listenType.value.rawValue,quarter: weakSelf.viewModel.quarter.value!,week: weakSelf.viewModel.week.value!, day: weakSelf.viewModel.day.value!, teamIds: temIds, topicIds: topicIds,answerNumber: totalNum,correctNumber:weakSelf.viewModel.correctNum,studyTime:weakSelf.viewModel.times,schedule: weakSelf.maxPage).subscribe(onNext: { data in |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | } |
| | | for vc in weakSelf.navigationController?.viewControllers ?? []{ |
| | | if vc.isKind(of: HomeListenVC.self){ |
| | | weakSelf.navigationController?.popToViewController(vc, animated: true) |
| | | NotificationCenter.default.post(name: Refresh_ListenSchedule_Noti, object: nil) |
| | | break |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | NotificationCenter.default.post(name: MeUserInfoUpdate_Noti, object: nil) |
| | | @objc func beAgaionAction(){ |
| | | CommonAlertView.show(content: "是否重新开始答题?确认后将清空当前答题进度") {[unowned self] in |
| | | let day = self.viewModel.day.value ?? 0 |
| | | let week = self.viewModel.week.value ?? 0 |
| | | let type = self.viewModel.listenType.value.rawValue |
| | | Services.restart(day: day, type: type, week: week).subscribe(onNext: {[unowned self]_ in |
| | | self.pageVC.scroll(toPage: 0, animation: true) |
| | | if let m = (self.data as? ListenNewModel){ |
| | | for v in m.list{ |
| | | v.status = 1 |
| | | } |
| | | self.restore() |
| | | self.setPages() |
| | | self.collection_card.reloadData() |
| | | NotificationCenter.default.post(name: ResetLession_Noti, object: nil) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | } |
| | | |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | case .game1,.game2,.story1,.story2: |
| | | Services.exitGameOrStory(studyTime: weakSelf.viewModel.times).subscribe(onNext: { _ in |
| | | @objc func nextAction(){ |
| | | listenFightLine = .next |
| | | if viewModel.listenType.value == .story2{ |
| | | |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | } |
| | | for vc in weakSelf.navigationController?.viewControllers ?? []{ |
| | | if vc.isKind(of: HomeListenVC.self){ |
| | | weakSelf.navigationController?.popToViewController(vc, animated: true) |
| | | NotificationCenter.default.post(name: Refresh_ListenSchedule_Noti, object: nil) |
| | | break |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | guard (pageVC.currentController as! HomeListenStory_2_VC).isPlayEnd else { |
| | | alert(msg: "请听完");return |
| | | } |
| | | |
| | | @objc func nextAction(){ |
| | | listenFightLine = .next |
| | | if viewModel.listenType.value == .story2{ |
| | | let v = data as! Listen1Model |
| | | var dict = Dictionary<String,Any>() |
| | | dict["storyId"] = v.data?.id ?? 0 |
| | | dict["storyIntegral"] = v.data?.lookIntegral ?? 0 |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: data) |
| | | } |
| | | } |
| | | |
| | | guard (pageVC.currentController as! HomeListenStory_2_VC).isPlayEnd else { |
| | | alert(msg: "请听完");return |
| | | } |
| | | @objc func beforeAction_mini(){ |
| | | if viewModel.listenType.value == .lesson1{ |
| | | if let vc = pageVC.currentController as? HomeListenFight_lesson_1_VC{ |
| | | print("---->进入") |
| | | vc.tobefore() |
| | | } |
| | | } |
| | | } |
| | | |
| | | let v = data as! Listen1Model |
| | | var dict = Dictionary<String,Any>() |
| | | dict["storyId"] = v.data?.id ?? 0 |
| | | dict["storyIntegral"] = v.data?.lookIntegral ?? 0 |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: data) |
| | | } |
| | | } |
| | | @objc func beforeAction(){ |
| | | |
| | | @objc func beforeAction_mini(){ |
| | | if viewModel.listenType.value == .lesson1{ |
| | | if let vc = pageVC.currentController as? HomeListenFight_lesson_1_VC{ |
| | | print("---->进入") |
| | | vc.tobefore() |
| | | } |
| | | } |
| | | } |
| | | listenFightLine = .before |
| | | let beforePage = max(0, viewModel.currentPage.value - 1) |
| | | switch viewModel.listenType.value { |
| | | case .lesson1: |
| | | if !(pageVC.currentController as! HomeListenFight_lesson_1_VC).isListen{ |
| | | alert(msg: "请听完");return |
| | | } |
| | | |
| | | @objc func beforeAction(){ |
| | | let temp = viewModel.answerCount.value - 1 |
| | | viewModel.answerCount.accept(max(1,temp)) |
| | | case .lesson2: |
| | | let temp = (beforePage * 4) + 1 |
| | | viewModel.answerCount.accept(max(1,temp)) |
| | | default:break |
| | | } |
| | | |
| | | listenFightLine = .before |
| | | let beforePage = max(0, viewModel.currentPage.value - 1) |
| | | switch viewModel.listenType.value { |
| | | case .lesson1: |
| | | if !(pageVC.currentController as! HomeListenFight_lesson_1_VC).isListen{ |
| | | alert(msg: "请听完");return |
| | | } |
| | | |
| | | let temp = viewModel.answerCount.value - 1 |
| | | viewModel.answerCount.accept(max(1,temp)) |
| | | case .lesson2: |
| | | let temp = (beforePage * 4) + 1 |
| | | viewModel.answerCount.accept(max(1,temp)) |
| | | default:break |
| | | } |
| | | pageVC.scroll(toPage: beforePage, animation: true) |
| | | viewModel.currentPage.accept(beforePage) |
| | | |
| | | |
| | | pageVC.scroll(toPage: beforePage, animation: true) |
| | | viewModel.currentPage.accept(beforePage) |
| | | if viewModel.listenType.value == .story2{ |
| | | guard (pageVC.currentController as! HomeListenStory_2_VC).isPlayEnd else { |
| | | alert(msg: "请听完");return |
| | | } |
| | | } |
| | | |
| | | if viewModel.listenType.value == .lesson1{ |
| | | let currentVC = pageVC.currentController as! HomeListenFight_lesson_1_VC |
| | | // if (viewModel.answerCount.value - 1 ) % 4 != 0 || viewModel.answerCount.value <= 4{ |
| | | currentVC.tobefore();return |
| | | // } |
| | | } |
| | | |
| | | if viewModel.listenType.value == .story2{ |
| | | guard (pageVC.currentController as! HomeListenStory_2_VC).isPlayEnd else { |
| | | alert(msg: "请听完");return |
| | | } |
| | | } |
| | | if viewModel.listenType.value == .lesson3{ |
| | | (pageVC.currentController as! HomeListenFight_lesson_3_VC).restore() |
| | | } |
| | | |
| | | if viewModel.listenType.value == .lesson1{ |
| | | let currentVC = pageVC.currentController as! HomeListenFight_lesson_1_VC |
| | | // if (viewModel.answerCount.value - 1 ) % 4 != 0 || viewModel.answerCount.value <= 4{ |
| | | currentVC.tobefore();return |
| | | // } |
| | | } |
| | | if viewModel.listenType.value == .story2{ |
| | | btn_next.isHidden = false |
| | | } |
| | | |
| | | if viewModel.listenType.value == .lesson3{ |
| | | (pageVC.currentController as! HomeListenFight_lesson_3_VC).restore() |
| | | } |
| | | btn_exit.setTitle("退出", for: .normal) |
| | | } |
| | | |
| | | if viewModel.listenType.value == .story2{ |
| | | btn_next.isHidden = false |
| | | } |
| | | private func restore(){ |
| | | if let vc = pageVC.currentController as? HomeListenFight_lesson_1_VC{ |
| | | vc.restore() |
| | | } |
| | | if let vc = pageVC.currentController as? HomeListenFight_lesson_2_VC{ |
| | | vc.restore() |
| | | } |
| | | if let vc = pageVC.currentController as? HomeListenFight_lesson_3_VC{ |
| | | vc.restore() |
| | | } |
| | | if let vc = pageVC.currentController as? HomeListenFight_lesson_4_VC{ |
| | | vc.restore() |
| | | } |
| | | if let vc = pageVC.currentController as? HomeListenFight_lesson_5_VC{ |
| | | vc.restore() |
| | | } |
| | | |
| | | btn_exit.setTitle("退出", for: .normal) |
| | | } |
| | | if let vc = pageVC.currentController as? HomeListenStory_1_VC{ |
| | | vc.restore() |
| | | } |
| | | if let vc = pageVC.currentController as? HomeListenStory_2_VC{ |
| | | vc.restore() |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFightVC:FFPageViewControllerDelegate{ |
| | | func totalPagesOfpageViewController(_ pageViewConteoller: FFPageViewController) -> UInt { |
| | | func totalPagesOfpageViewController(_ pageViewConteoller: FFPageViewController) -> UInt { |
| | | |
| | | switch viewModel.listenType.value { |
| | | case .lesson1,.lesson2,.lesson3,.lesson4,.lesson5:return UInt((data as! ListenNewModel).subjectList.count) |
| | | case .story1,.story2: |
| | | return UInt((data as! Listen1Model).storyList.count) |
| | | default:break |
| | | } |
| | | switch viewModel.listenType.value { |
| | | case .lesson1,.lesson2,.lesson3,.lesson4,.lesson5:return UInt((data as! ListenNewModel).subjectList.count) |
| | | case .story1,.story2: |
| | | return UInt((data as! Listen1Model).storyList.count) |
| | | default:break |
| | | } |
| | | |
| | | |
| | | //超级听力,只有一页 |
| | | if viewModel.listenType.value == .game1 || viewModel.listenType.value == .game2{ |
| | | return 1 |
| | | } |
| | | return UInt(viewModel.maxPage.value) |
| | | } |
| | | //超级听力,只有一页 |
| | | if viewModel.listenType.value == .game1 || viewModel.listenType.value == .game2{ |
| | | return 1 |
| | | } |
| | | return UInt(viewModel.maxPage.value) |
| | | } |
| | | |
| | | func pageViewController(_ pageViewController: FFPageViewController, currentPageChanged currentPage: Int) { |
| | | func pageViewController(_ pageViewController: FFPageViewController, currentPageChanged currentPage: Int) { |
| | | |
| | | if listenFightLine == .before{ |
| | | if let vc = pageViewController.currentController as? HomeListenFight_lesson_1_VC{ |
| | | vc.restore() |
| | | } |
| | | if let vc = pageViewController.currentController as? HomeListenFight_lesson_2_VC{ |
| | | vc.restore() |
| | | } |
| | | if let vc = pageViewController.currentController as? HomeListenFight_lesson_3_VC{ |
| | | vc.restore() |
| | | } |
| | | if let vc = pageViewController.currentController as? HomeListenFight_lesson_4_VC{ |
| | | vc.restore() |
| | | } |
| | | if let vc = pageViewController.currentController as? HomeListenFight_lesson_5_VC{ |
| | | vc.restore() |
| | | } |
| | | if listenFightLine == .before{ |
| | | restore() |
| | | } |
| | | } |
| | | |
| | | if let vc = pageViewController.currentController as? HomeListenStory_1_VC{ |
| | | vc.restore() |
| | | } |
| | | if let vc = pageViewController.currentController as? HomeListenStory_2_VC{ |
| | | vc.restore() |
| | | } |
| | | } |
| | | } |
| | | func pageViewController(_ pageViewConteoller: FFPageViewController, controllerForPage page: Int) -> UIViewController { |
| | | if viewModel.listenType.value == .lesson1{ |
| | | let vc = HomeListenFight_lesson_1_VC(page: page,listenNewModel:data as! ListenNewModel) |
| | | vc.teamScheduleModel = teamScheduleModel |
| | | vc.rootViewModel = viewModel |
| | | vc.handleClouseAction {[unowned self] in |
| | | self.setPages() |
| | | self.collection_card.reloadData() |
| | | } |
| | | return vc |
| | | } |
| | | |
| | | func pageViewController(_ pageViewConteoller: FFPageViewController, controllerForPage page: Int) -> UIViewController { |
| | | if viewModel.listenType.value == .lesson1{ |
| | | let vc = HomeListenFight_lesson_1_VC(page: page,listenNewModel:data as! ListenNewModel) |
| | | vc.teamScheduleModel = teamScheduleModel |
| | | vc.rootViewModel = viewModel |
| | | return vc |
| | | } |
| | | if viewModel.listenType.value == .lesson2{ |
| | | let vc = HomeListenFight_lesson_2_VC(page: page,listenNewModel:data as! ListenNewModel) |
| | | vc.teamScheduleModel = teamScheduleModel |
| | | vc.rootViewModel = viewModel |
| | | vc.handleClouseAction {[unowned self] in |
| | | self.setPages() |
| | | self.collection_card.reloadData() |
| | | } |
| | | return vc |
| | | } |
| | | |
| | | if viewModel.listenType.value == .lesson2{ |
| | | let vc = HomeListenFight_lesson_2_VC(page: page,listenNewModel:data as! ListenNewModel) |
| | | vc.teamScheduleModel = teamScheduleModel |
| | | vc.rootViewModel = viewModel |
| | | return vc |
| | | } |
| | | if viewModel.listenType.value == .lesson3{ |
| | | let vc = HomeListenFight_lesson_3_VC(page: page, listenNewModel: data as! ListenNewModel) |
| | | vc.teamScheduleModel = teamScheduleModel |
| | | vc.rootViewModel = viewModel |
| | | vc.handleClouseAction {[unowned self] in |
| | | self.setPages() |
| | | self.collection_card.reloadData() |
| | | } |
| | | return vc |
| | | } |
| | | |
| | | if viewModel.listenType.value == .lesson3{ |
| | | let vc = HomeListenFight_lesson_3_VC(page: page, listenNewModel: data as! ListenNewModel) |
| | | vc.teamScheduleModel = teamScheduleModel |
| | | vc.rootViewModel = viewModel |
| | | return vc |
| | | } |
| | | if viewModel.listenType.value == .lesson4{ |
| | | let vc = HomeListenFight_lesson_4_VC(page: page, listenNewModel: data as! ListenNewModel) |
| | | vc.teamScheduleModel = teamScheduleModel |
| | | vc.rootViewModel = viewModel |
| | | vc.handleClouseAction {[unowned self] in |
| | | self.setPages() |
| | | self.collection_card.reloadData() |
| | | } |
| | | return vc |
| | | } |
| | | |
| | | if viewModel.listenType.value == .lesson4{ |
| | | let vc = HomeListenFight_lesson_4_VC(page: page, listenNewModel: data as! ListenNewModel) |
| | | vc.teamScheduleModel = teamScheduleModel |
| | | vc.rootViewModel = viewModel |
| | | return vc |
| | | } |
| | | if viewModel.listenType.value == .lesson5{ |
| | | let vc = HomeListenFight_lesson_5_VC(page: page, listenNewModel: data as! ListenNewModel) |
| | | vc.teamScheduleModel = teamScheduleModel |
| | | vc.rootViewModel = viewModel |
| | | vc.handleClouseAction {[unowned self] in |
| | | self.setPages() |
| | | self.collection_card.reloadData() |
| | | } |
| | | return vc |
| | | } |
| | | |
| | | if viewModel.listenType.value == .lesson5{ |
| | | let vc = HomeListenFight_lesson_5_VC(page: page, listenNewModel: data as! ListenNewModel) |
| | | vc.teamScheduleModel = teamScheduleModel |
| | | vc.rootViewModel = viewModel |
| | | return vc |
| | | } |
| | | if viewModel.listenType.value == .game1{ |
| | | if data == nil{return UIViewController()} |
| | | let vc = HomeListenGame_1_VC(listen1Model: data as! Listen1Model) |
| | | vc.rootViewModel = viewModel |
| | | return vc |
| | | } |
| | | |
| | | if viewModel.listenType.value == .game1{ |
| | | if data == nil{return UIViewController()} |
| | | let vc = HomeListenGame_1_VC(listen1Model: data as! Listen1Model) |
| | | vc.rootViewModel = viewModel |
| | | return vc |
| | | } |
| | | if viewModel.listenType.value == .game2{ |
| | | let vc = HomeListenGame_2_VC(listen1Model: data as! Listen1Model) |
| | | vc.rootViewModel = viewModel |
| | | return vc |
| | | } |
| | | |
| | | if viewModel.listenType.value == .game2{ |
| | | let vc = HomeListenGame_2_VC(listen1Model: data as! Listen1Model) |
| | | vc.rootViewModel = viewModel |
| | | return vc |
| | | } |
| | | if viewModel.listenType.value == .story1{ |
| | | let vc = HomeListenStory_1_VC(page: page, listen1Model: data as! Listen1Model) |
| | | vc.rootViewModel = viewModel |
| | | return vc |
| | | } |
| | | |
| | | if viewModel.listenType.value == .story1{ |
| | | let vc = HomeListenStory_1_VC(page: page, listen1Model: data as! Listen1Model) |
| | | vc.rootViewModel = viewModel |
| | | return vc |
| | | } |
| | | if viewModel.listenType.value == .story2{ |
| | | let vc = HomeListenStory_2_VC(page: page, listen1Model: data as! Listen1Model) |
| | | vc.rootViewModel = viewModel |
| | | return vc |
| | | } |
| | | |
| | | if viewModel.listenType.value == .story2{ |
| | | let vc = HomeListenStory_2_VC(page: page, listen1Model: data as! Listen1Model) |
| | | vc.rootViewModel = viewModel |
| | | return vc |
| | | } |
| | | |
| | | let vc = UIViewController() |
| | | return vc |
| | | } |
| | | let vc = UIViewController() |
| | | return vc |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFightVC:UICollectionViewDelegate{ |
| | | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
| | | if let m = data as? ListenNewModel{ |
| | | guard m.list[indexPath.row].status != 1 else{return} |
| | | guard pageVC.currentPage != indexPath.row else {return} |
| | | pageVC.scroll(toPage: indexPath.row, animation: true) |
| | | //todo |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFightVC:UICollectionViewDataSource{ |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | if let m = data as? ListenNewModel{ |
| | | if viewModel.listenType.value == .lesson1{ |
| | | return pages.count |
| | | } |
| | | return pages.count |
| | | } |
| | | return 0 |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_CardItemCCell", for: indexPath) as! CardItemCCell |
| | | if let m = data as? ListenNewModel{ |
| | | let model = pages[indexPath.row] |
| | | cell.titleL.text = "\(indexPath.row + 1)" |
| | | |
| | | if viewModel.listenType.value == .lesson1{ |
| | | if model.filter({$0.status == 2}).count == 4{ |
| | | cell.titleL.textColor = UIColor(hexString: "#52C41A") |
| | | cell.titleL.jq_borderColor = UIColor(hexString: "#52C41A") |
| | | }else if model.filter({$0.status == 3}).count >= 1 { |
| | | cell.titleL.textColor = UIColor(hexString: "#FF4D4F") |
| | | cell.titleL.jq_borderColor = UIColor(hexString: "#52C41A") |
| | | }else{ |
| | | cell.titleL.textColor = .black.withAlphaComponent(0.25) |
| | | cell.titleL.jq_borderColor = .black.withAlphaComponent(0.25) |
| | | } |
| | | }else{ |
| | | //状态1灰色未答题 2绿色正确 3红色错误 |
| | | switch model.first!.status{ |
| | | case 1: |
| | | cell.titleL.textColor = .black.withAlphaComponent(0.25) |
| | | cell.titleL.jq_borderColor = .black.withAlphaComponent(0.25) |
| | | case 2: |
| | | cell.titleL.textColor = UIColor(hexString: "#52C41A") |
| | | cell.titleL.jq_borderColor = UIColor(hexString: "#52C41A") |
| | | case 3: |
| | | cell.titleL.textColor = UIColor(hexString: "#FF4D4F") |
| | | cell.titleL.jq_borderColor = UIColor(hexString: "#52C41A") |
| | | default:break |
| | | } |
| | | } |
| | | } |
| | | return cell |
| | | } |
| | | } |
| | | |
| | | class CardItemCCell:UICollectionViewCell{ |
| | | |
| | | var titleL:UILabel! |
| | | |
| | | override init(frame: CGRect) { |
| | | super.init(frame: frame) |
| | | |
| | | titleL = UILabel() |
| | | titleL.text = "1" |
| | | titleL.backgroundColor = .white |
| | | titleL.jq_borderColor = .black.withAlphaComponent(0.15) |
| | | titleL.jq_borderWidth = 0.5 |
| | | titleL.font = .systemFont(ofSize: 13,weight: .semibold) |
| | | titleL.textAlignment = .center |
| | | contentView.addSubview(titleL) |
| | | titleL.snp.makeConstraints { make in |
| | | make.edges.equalToSuperview() |
| | | } |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | } |
| | |
| | | import QMUIKit |
| | | |
| | | class HomeListenFight_lesson_2_VC: BaseVC { |
| | | |
| | | private var viewModel = FightAnswerViewModel() |
| | | private var listenNewModel:ListenNewModel! |
| | | private var page:Int! |
| | | var rootViewModel:HomeListenFightViewModel! |
| | | var teamScheduleModel:TeamScheduleModel? |
| | | private var tempViews = [StudyHandleView]() |
| | | |
| | | private var playedIndex = Set<Int>() //已经播放过的view |
| | | private var voicePlayer = VoicePlayer.share() |
| | | private var isAnsterModel = Set<Listen1SubModel>() |
| | | |
| | | private var viewModel = FightAnswerViewModel() |
| | | private var listenNewModel:ListenNewModel! |
| | | private var page:Int! |
| | | var rootViewModel:HomeListenFightViewModel! |
| | | var teamScheduleModel:TeamScheduleModel? |
| | | private var tempViews = [StudyHandleView]() |
| | | |
| | | private var playedIndex = Set<Int>() //已经播放过的view |
| | | private var voicePlayer = VoicePlayer.share() |
| | | private var isAnsterModel = Set<Listen1SubModel>() |
| | | private var isOpen:Bool = false //是否展示标题文本 |
| | | |
| | | private lazy var stackView:UIStackView = { |
| | | let stackView = UIStackView() |
| | | stackView.spacing = 78 |
| | | stackView.distribution = .equalSpacing |
| | | return stackView |
| | | }() |
| | | |
| | | private lazy var collectionView:UICollectionView = { |
| | | let flowLayout = UICollectionViewFlowLayout() |
| | | let w = (JQ_ScreenW - 194 * 2 - 25) / 2 |
| | | flowLayout.itemSize = CGSize(width: w, height: w * 0.745) |
| | | flowLayout.minimumLineSpacing = 25 |
| | | flowLayout.minimumInteritemSpacing = 25 |
| | | flowLayout.scrollDirection = .vertical |
| | | let collection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) |
| | | collection.register(UINib(nibName: "ListenFight_lesson_1_CCell", bundle: nil), forCellWithReuseIdentifier: "_ListenFight_lesson_1_CCell") |
| | | collection.isScrollEnabled = false |
| | | return collection |
| | | }() |
| | | |
| | | |
| | | required init(page:Int,listenNewModel:ListenNewModel){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.page = page |
| | | self.listenNewModel = listenNewModel |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | navigationItem.titleView = UIView() |
| | | |
| | | private var studieds = 0 |
| | | private var handleClouse:(()->Void)? |
| | | |
| | | private lazy var stackView:UIStackView = { |
| | | let stackView = UIStackView() |
| | | stackView.spacing = 78 |
| | | stackView.distribution = .equalSpacing |
| | | return stackView |
| | | }() |
| | | |
| | | private lazy var collectionView:UICollectionView = { |
| | | let flowLayout = UICollectionViewFlowLayout() |
| | | let w = (JQ_ScreenW - 194 * 2 - 25) / 2 |
| | | flowLayout.itemSize = CGSize(width: w, height: w * 0.745) |
| | | flowLayout.minimumLineSpacing = 25 |
| | | flowLayout.minimumInteritemSpacing = 25 |
| | | flowLayout.scrollDirection = .vertical |
| | | let collection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) |
| | | collection.register(UINib(nibName: "ListenFight_lesson_1_CCell", bundle: nil), forCellWithReuseIdentifier: "_ListenFight_lesson_1_CCell") |
| | | collection.isScrollEnabled = false |
| | | return collection |
| | | }() |
| | | |
| | | |
| | | required init(page:Int,listenNewModel:ListenNewModel){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.page = page |
| | | self.listenNewModel = listenNewModel |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | navigationItem.titleView = UIView() |
| | | |
| | | Services.getIsOpen().subscribe(onNext: {data in |
| | | self.isOpen = data.data ?? false |
| | | self.collectionView.reloadData() |
| | | }).disposed(by: disposeBag) |
| | | |
| | | |
| | | //回传记录,始终保持答题进度 |
| | | if let team = teamScheduleModel{ |
| | | for teamId in team.teamIds{ |
| | | for v in listenNewModel.subjectList[page]{ |
| | | if team.topicIds.contains(v.id){ |
| | | rootViewModel.insertCorrectAnswer(teamId: "\(teamId)", answerId: v.id) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | super.viewDidAppear(animated) |
| | | VoicePlayer.share().delegate = self |
| | | } |
| | | |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | VoicePlayer.share().delegate = nil |
| | | VoicePlayer.share().playerInterrupt() |
| | | } |
| | | |
| | | func restore(){ |
| | | playedIndex.removeAll() |
| | | tempViews.removeAll() |
| | | |
| | | for subView in view.subviews{ |
| | | if subView is StudyHandleView{ |
| | | subView.removeFromSuperview() |
| | | } |
| | | } |
| | | setUI() |
| | | collectionView.reloadData() |
| | | |
| | | } |
| | | |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | | let flowLayout = self.collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | let w = (self.collectionView.size.width - flowLayout.minimumLineSpacing) / 2 |
| | | let h = (self.collectionView.size.height - flowLayout.minimumInteritemSpacing) / 2 |
| | | |
| | | if flowLayout.itemSize.width != w || flowLayout.itemSize.height != h{ |
| | | flowLayout.itemSize = CGSize(width: w, height: h) |
| | | collectionView.reloadData() |
| | | } |
| | | } |
| | | |
| | | override func setUI() { |
| | | super.setUI() |
| | | |
| | | viewModel.selectIndex.accept(IndexPath(row: 0, section: 0)) |
| | | |
| | | if !view.subviews.contains(collectionView){ |
| | | collectionView.delegate = self |
| | | collectionView.dataSource = self |
| | | collectionView.backgroundColor = UIColor(hexStr: "#C3BFB3") |
| | | view.addSubview(collectionView) |
| | | } |
| | | |
| | | collectionView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(101) |
| | | make.left.equalTo(194) |
| | | make.right.equalTo(-194) |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | |
| | | if !view.subviews.contains(stackView){ |
| | | view.addSubview(stackView) |
| | | } |
| | | |
| | | stackView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(24) |
| | | make.centerX.equalToSuperview() |
| | | make.height.equalTo(52) |
| | | make.width.greaterThanOrEqualTo(100) |
| | | } |
| | | |
| | | addStackView() |
| | | |
| | | } |
| | | |
| | | private func addStackView(){ |
| | | |
| | | for subV in stackView.arrangedSubviews{ |
| | | subV.removeFromSuperview() |
| | | } |
| | | tempViews.removeAll() |
| | | |
| | | for index in 0...2{ |
| | | let handleView = StudyHandleView.jq_loadNibView() |
| | | handleView.listenType = .lesson2 |
| | | handleView.view_choose.alpha = 0 |
| | | handleView.btn_choose.tag = 10 + index |
| | | handleView.tag = 20 + index |
| | | |
| | | let row = viewModel.selectIndex.value!.row |
| | | let model = listenNewModel.subjectList[page][row] |
| | | |
| | | if index == 0{ |
| | | handleView.vioceSoundUrl = model.correct |
| | | } |
| | | |
| | | if index == 1{ |
| | | handleView.vioceSoundUrl = model.error.components(separatedBy: ",").first |
| | | } |
| | | |
| | | if index == 2{ |
| | | handleView.vioceSoundUrl = model.error.components(separatedBy: ",").last |
| | | } |
| | | |
| | | handleView.playAt {[weak self] index in |
| | | self?.playedIndex.insert(index) |
| | | } |
| | | |
| | | |
| | | handleView.chooseClouse {[weak self] btn in |
| | | guard let weakSelf = self else { return } |
| | | if weakSelf.playedIndex.count != 3{ |
| | | handleView.btn_choose.isSelected = false |
| | | alertError(msg: "请听完");return |
| | | } |
| | | handleView.view_choose.alpha = 1 |
| | | var lessionType:Fight_lessonType = .none |
| | | if handleView.vioceSoundUrl == weakSelf.listenNewModel.subjectList[weakSelf.page][row].correct{ |
| | | lessionType = .success |
| | | weakSelf.voicePlayer.playSuccessVoice() |
| | | let teamId = weakSelf.listenNewModel.data?.id.components(separatedBy: ",")[weakSelf.page] |
| | | let answerId = weakSelf.listenNewModel.subjectList[weakSelf.page][row].id |
| | | weakSelf.rootViewModel.insertCorrectAnswer(teamId: teamId, answerId: answerId) |
| | | }else{ |
| | | lessionType = .fail |
| | | // 重置按钮至最初样式 |
| | | weakSelf.playedIndex.removeAll() |
| | | for sub in weakSelf.stackView.arrangedSubviews as! [StudyHandleView]{ |
| | | sub.btn_pay.isEnabled = true |
| | | sub.btn_choose.isEnabled = true |
| | | sub.resetView() |
| | | sub.view_choose.alpha = 0 |
| | | } |
| | | weakSelf.voicePlayer.playFailVoice() |
| | | } |
| | | |
| | | switch lessionType { |
| | | case .success: |
| | | weakSelf.rootViewModel.correctNum += 1 |
| | | handleView.btn_choose.isEnabled = false |
| | | handleView.btn_state.setImage(UIImage(named: "icon_success_small"), for: .normal) |
| | | UIView.animate(withDuration: 0.5) { |
| | | handleView.btn_state.alpha = 1 |
| | | } |
| | | |
| | | let i = ceil(handleView.x / 300).int |
| | | if let cell = self?.collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_1_CCell", for: self!.viewModel.selectIndex.value!) as? ListenFight_lesson_1_CCell{ |
| | | self?.viewModel.answerType.accept(.success) |
| | | self?.answerSuccess(cell,index: i) |
| | | } |
| | | case .fail: |
| | | weakSelf.rootViewModel.errorNum += 1 |
| | | handleView.btn_state.setImage(UIImage(named: "icon_waring_small"), for: .normal) |
| | | UIView.animate(withDuration: 0.5) { |
| | | handleView.btn_state.alpha = 1 |
| | | } |
| | | UIView.animate(withDuration: 0.5, delay: 1.2, options: .layoutSubviews) { |
| | | handleView.btn_pay.alpha = 1 |
| | | handleView.btn_voice.alpha = 1 |
| | | handleView.btn_state.alpha = 0 |
| | | } completion: { _ in |
| | | handleView.btn_choose.isSelected = false |
| | | } |
| | | default: |
| | | handleView.btn_state.setImage(nil, for: .normal) |
| | | } |
| | | } |
| | | |
| | | handleView.snp.makeConstraints { make in |
| | | make.height.equalTo(52) |
| | | make.width.greaterThanOrEqualTo(221) |
| | | } |
| | | tempViews.append(handleView) |
| | | } |
| | | |
| | | tempViews.shuffle() //乱序 |
| | | stackView.addArrangedSubviews(tempViews) |
| | | } |
| | | |
| | | private func answerSuccess(_ cell:ListenFight_lesson_1_CCell,index:Int){ |
| | | |
| | | let studyHandleView = stackView.arrangedSubviews[index] as! StudyHandleView |
| | | |
| | | let bounds = studyHandleView.convert(studyHandleView.bounds, to: self.view) |
| | | |
| | | //copy试图放在上面进行覆盖 |
| | | let copyHandleView = studyHandleView.copyView() |
| | | copyHandleView.view_choose.isHidden = true |
| | | copyHandleView.listenType = .lesson2 |
| | | copyHandleView.isplaying() |
| | | copyHandleView.vioceSoundUrl = listenNewModel.subjectList[page][self.viewModel.selectIndex.value!.row].correct |
| | | copyHandleView.jq_cornerRadius = 0 |
| | | view.addSubview(copyHandleView) |
| | | view.layoutIfNeeded() |
| | | |
| | | copyHandleView.snp.makeConstraints { make in |
| | | make.top.equalToSuperview().offset(bounds.origin.y) |
| | | make.left.equalToSuperview().offset(bounds.origin.x) |
| | | make.width.equalTo(159) |
| | | make.height.equalTo(52) |
| | | } |
| | | |
| | | view.layoutIfNeeded() |
| | | |
| | | let v = cell.view_topHandle.convert(cell.bounds, to: self.view) |
| | | UIView.animate(withDuration: 0.6) { |
| | | copyHandleView.snp.updateConstraints { make in |
| | | make.top.equalTo(self.view).offset(v.origin.y + UIDevice.jq_safeEdges.top + 101 + 50) |
| | | make.left.equalToSuperview().offset(v.origin.x + 194) |
| | | make.width.equalTo(v.size.width - 10) |
| | | make.height.equalTo(40) |
| | | } |
| | | self.view.layoutIfNeeded() |
| | | }completion: { complete in |
| | | if complete{ |
| | | self.voicePlayer.playerAt(url: self.listenNewModel.subjectList[self.page][self.viewModel.selectIndex.value!.row].correct) |
| | | } |
| | | } |
| | | } |
| | | |
| | | private func resetStackView(){ |
| | | playedIndex.removeAll() |
| | | let newRow = viewModel.selectIndex.value!.row+1 |
| | | if newRow >= listenNewModel.subjectList[page].count{ //防止坐标越界 |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: nil);return |
| | | } |
| | | |
| | | collectionView.reloadData() |
| | | viewModel.selectIndex.accept(IndexPath(row: newRow, section: 0)) |
| | | addStackView() |
| | | } |
| | | |
| | | override func setRx() {} |
| | | |
| | | |
| | | //回传记录,始终保持答题进度 |
| | | if let team = teamScheduleModel{ |
| | | for teamId in team.teamIds{ |
| | | for v in listenNewModel.subjectList[page]{ |
| | | if team.topicIds.contains(v.id){ |
| | | rootViewModel.insertCorrectAnswer(teamId: "\(teamId)", answerId: v.id) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | super.viewDidAppear(animated) |
| | | VoicePlayer.share().delegate = self |
| | | } |
| | | |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | VoicePlayer.share().delegate = nil |
| | | VoicePlayer.share().playerInterrupt() |
| | | } |
| | | |
| | | func restore(){ |
| | | playedIndex.removeAll() |
| | | tempViews.removeAll() |
| | | |
| | | for subView in view.subviews{ |
| | | if subView is StudyHandleView{ |
| | | subView.removeFromSuperview() |
| | | } |
| | | } |
| | | setUI() |
| | | collectionView.reloadData() |
| | | |
| | | } |
| | | |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | | let flowLayout = self.collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | let w = (self.collectionView.size.width - flowLayout.minimumLineSpacing) / 2 |
| | | let h = (self.collectionView.size.height - flowLayout.minimumInteritemSpacing) / 2 |
| | | |
| | | if flowLayout.itemSize.width != w || flowLayout.itemSize.height != h{ |
| | | flowLayout.itemSize = CGSize(width: w, height: h) |
| | | collectionView.reloadData() |
| | | } |
| | | } |
| | | |
| | | override func setUI() { |
| | | super.setUI() |
| | | |
| | | viewModel.selectIndex.accept(IndexPath(row: 0, section: 0)) |
| | | |
| | | if !view.subviews.contains(collectionView){ |
| | | collectionView.delegate = self |
| | | collectionView.dataSource = self |
| | | collectionView.backgroundColor = UIColor(hexStr: "#C3BFB3") |
| | | view.addSubview(collectionView) |
| | | } |
| | | |
| | | collectionView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(101) |
| | | make.left.equalTo(194) |
| | | make.right.equalTo(-194) |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | |
| | | if !view.subviews.contains(stackView){ |
| | | view.addSubview(stackView) |
| | | } |
| | | |
| | | stackView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(24) |
| | | make.centerX.equalToSuperview() |
| | | make.height.equalTo(40) |
| | | make.width.greaterThanOrEqualTo(125) |
| | | } |
| | | |
| | | addStackView() |
| | | } |
| | | |
| | | private func addStackView(){ |
| | | |
| | | for subV in stackView.arrangedSubviews{ |
| | | subV.removeFromSuperview() |
| | | } |
| | | tempViews.removeAll() |
| | | |
| | | for index in 0...2{ |
| | | let handleView = StudyHandleView.jq_loadNibView() |
| | | handleView.listenType = .lesson2 |
| | | handleView.view_choose.alpha = 0 |
| | | handleView.btn_choose.tag = 10 + index |
| | | handleView.tag = 20 + index |
| | | |
| | | let row = viewModel.selectIndex.value!.row |
| | | let model = listenNewModel.subjectList[page][row] |
| | | |
| | | if index == 0{ |
| | | handleView.vioceSoundUrl = model.correct |
| | | } |
| | | |
| | | if index == 1{ |
| | | handleView.vioceSoundUrl = model.error.components(separatedBy: ",").first |
| | | } |
| | | |
| | | if index == 2{ |
| | | handleView.vioceSoundUrl = model.error.components(separatedBy: ",").last |
| | | } |
| | | |
| | | handleView.playAt {[weak self] index in |
| | | self?.playedIndex.insert(index) |
| | | } |
| | | |
| | | |
| | | handleView.chooseClouse {[weak self] btn in |
| | | guard let weakSelf = self else { return } |
| | | if weakSelf.playedIndex.count < 3{ |
| | | handleView.btn_choose.isSelected = false |
| | | alertError(msg: "请听完");return |
| | | } |
| | | handleView.view_choose.alpha = 1 |
| | | var lessionType:Fight_lessonType = .none |
| | | if handleView.vioceSoundUrl == weakSelf.listenNewModel.subjectList[weakSelf.page][row].correct{ |
| | | lessionType = .success |
| | | weakSelf.voicePlayer.playSuccessVoice() |
| | | let teamId = weakSelf.listenNewModel.data?.id.components(separatedBy: ",")[weakSelf.page] |
| | | let answerId = weakSelf.listenNewModel.subjectList[weakSelf.page][row].id |
| | | weakSelf.rootViewModel.insertCorrectAnswer(teamId: teamId, answerId: answerId) |
| | | }else{ |
| | | lessionType = .fail |
| | | // 重置按钮至最初样式 |
| | | weakSelf.playedIndex.removeAll() |
| | | for sub in weakSelf.stackView.arrangedSubviews as! [StudyHandleView]{ |
| | | sub.btn_pay.isEnabled = true |
| | | sub.btn_choose.isEnabled = true |
| | | sub.resetView() |
| | | sub.view_choose.alpha = 0 |
| | | } |
| | | weakSelf.voicePlayer.playFailVoice() |
| | | } |
| | | |
| | | switch lessionType { |
| | | case .success: |
| | | weakSelf.rootViewModel.correctNum += 1 |
| | | handleView.btn_choose.isEnabled = false |
| | | handleView.btn_state.setImage(UIImage(named: "icon_success_small"), for: .normal) |
| | | UIView.animate(withDuration: 0.5) { |
| | | handleView.btn_state.alpha = 1 |
| | | } |
| | | |
| | | let i = ceil(handleView.x / 300).int |
| | | if let cell = self?.collectionView.cellForItem(at: self!.viewModel.selectIndex.value!) as? ListenFight_lesson_1_CCell{ |
| | | self?.viewModel.answerType.accept(.success) |
| | | self?.answerSuccess(cell,index: i) |
| | | |
| | | if let data = self?.listenNewModel.list[self!.page]{ |
| | | Services.answerQuestion(id: data.id, status: 2).subscribe(onNext: {_ in |
| | | self?.handleClouse?() |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | } |
| | | } |
| | | case .fail: |
| | | weakSelf.rootViewModel.errorNum += 1 |
| | | handleView.btn_state.setImage(UIImage(named: "icon_waring_small"), for: .normal) |
| | | UIView.animate(withDuration: 0.5) { |
| | | handleView.btn_state.alpha = 1 |
| | | } |
| | | UIView.animate(withDuration: 0.5, delay: 1.2, options: .layoutSubviews) { |
| | | handleView.btn_pay.alpha = 1 |
| | | handleView.btn_voice.alpha = 1 |
| | | handleView.btn_state.alpha = 0 |
| | | } completion: { _ in |
| | | handleView.btn_choose.isSelected = false |
| | | } |
| | | |
| | | if let data = self?.listenNewModel.list[self!.page]{ |
| | | Services.answerQuestion(id: data.id, status: 3).subscribe(onNext: {_ in |
| | | self?.handleClouse?() |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | } |
| | | |
| | | |
| | | default: |
| | | handleView.btn_state.setImage(nil, for: .normal) |
| | | } |
| | | } |
| | | |
| | | handleView.snp.makeConstraints { make in |
| | | make.height.equalTo(52) |
| | | make.width.greaterThanOrEqualTo(221) |
| | | } |
| | | tempViews.append(handleView) |
| | | } |
| | | |
| | | tempViews.shuffle() //乱序 |
| | | stackView.addArrangedSubviews(tempViews) |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+1) { |
| | | let temp = self.tempViews.first |
| | | temp?.isplaying() |
| | | self.voicePlayer.playerAt(url: temp?.vioceSoundUrl ?? "") |
| | | self.playedIndex.insert(0) |
| | | self.collectionView.reloadData() |
| | | } |
| | | } |
| | | |
| | | private func answerSuccess(_ cell:ListenFight_lesson_1_CCell,index:Int){ |
| | | studieds += 1 |
| | | let studyHandleView = stackView.arrangedSubviews[index] as! StudyHandleView |
| | | |
| | | let bounds = studyHandleView.convert(studyHandleView.bounds, to: self.view) |
| | | |
| | | //copy试图放在上面进行覆盖 |
| | | let copyHandleView = studyHandleView.copyView() |
| | | copyHandleView.view_choose.isHidden = true |
| | | copyHandleView.listenType = .lesson2 |
| | | copyHandleView.isplaying() |
| | | copyHandleView.vioceSoundUrl = listenNewModel.subjectList[page][self.viewModel.selectIndex.value!.row].correct |
| | | copyHandleView.jq_cornerRadius = 0 |
| | | view.addSubview(copyHandleView) |
| | | view.layoutIfNeeded() |
| | | |
| | | copyHandleView.snp.makeConstraints { make in |
| | | make.top.equalToSuperview().offset(bounds.origin.y) |
| | | make.left.equalToSuperview().offset(bounds.origin.x) |
| | | make.width.equalTo(159) |
| | | make.height.equalTo(52) |
| | | } |
| | | |
| | | view.layoutIfNeeded() |
| | | |
| | | let v = cell.view_topHandle.convert(cell.bounds, to: self.view) |
| | | UIView.animate(withDuration: 0.6) { |
| | | copyHandleView.snp.updateConstraints { make in |
| | | make.top.equalTo(self.view).offset(v.origin.y) |
| | | make.left.equalToSuperview().offset(v.origin.x) |
| | | make.width.equalTo(v.size.width - 10) |
| | | make.height.equalTo(40) |
| | | } |
| | | self.view.layoutIfNeeded() |
| | | }completion: { complete in |
| | | if complete{ |
| | | self.voicePlayer.playerAt(url: self.listenNewModel.subjectList[self.page][self.viewModel.selectIndex.value!.row].correct) |
| | | } |
| | | } |
| | | } |
| | | |
| | | private func resetStackView(){ |
| | | playedIndex.removeAll() |
| | | let newRow = viewModel.selectIndex.value!.row+1 |
| | | if newRow >= listenNewModel.subjectList[page].count{ //防止坐标越界 |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: nil);return |
| | | } |
| | | |
| | | collectionView.reloadData() |
| | | viewModel.selectIndex.accept(IndexPath(row: newRow, section: 0)) |
| | | addStackView() |
| | | } |
| | | |
| | | override func setRx() { |
| | | NotificationCenter.default.rx.notification(ResetLession_Noti).take(until: self.rx.deallocated).subscribe(onNext: {[weak self]_ in |
| | | self?.restore() |
| | | self?.viewDidLoad() |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | func handleClouseAction(clouse:@escaping ()->Void){ |
| | | self.handleClouse = clouse |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_2_VC:UICollectionViewDelegate{ |
| | | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
| | | viewModel.selectIndex.accept(indexPath) |
| | | } |
| | | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
| | | viewModel.selectIndex.accept(indexPath) |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_2_VC:UICollectionViewDataSource{ |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_1_CCell", for: indexPath) as! ListenFight_lesson_1_CCell |
| | | cell.jq_addShadows(shadowColor: .black.withAlphaComponent(0.31), corner: 5, radius: 5, offset: CGSize(width: 0, height: 1), opacity: 1) |
| | | cell.backgroundColor = .white |
| | | |
| | | if viewModel.selectIndex.value?.row == indexPath.row && isOpen{ |
| | | cell.label_title.isHidden = false |
| | | }else{ |
| | | cell.label_title.isHidden = true |
| | | } |
| | | |
| | | cell.setListen1SubModel(listenNewModel.subjectList[page][indexPath.row]) |
| | | cell.label_title.text = listenNewModel.subjectList[page][indexPath.row].name |
| | | return cell |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | return listenNewModel.subjectList[page].count |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_1_CCell", for: indexPath) as! ListenFight_lesson_1_CCell |
| | | cell.jq_addShadows(shadowColor: .black.withAlphaComponent(0.31), corner: 5, radius: 5, offset: CGSize(width: 0, height: 1), opacity: 1) |
| | | |
| | | if indexPath.row == studieds{ |
| | | cell.backgroundColor = .orange |
| | | }else{ |
| | | cell.backgroundColor = .white |
| | | } |
| | | |
| | | |
| | | |
| | | // if viewModel.selectIndex.value?.row == indexPath.row && isOpen{ |
| | | // cell.label_title.isHidden = false |
| | | // }else{ |
| | | cell.label_title.isHidden = true |
| | | // } |
| | | |
| | | cell.setListen1SubModel(listenNewModel.subjectList[page][indexPath.row]) |
| | | cell.label_title.text = listenNewModel.subjectList[page][indexPath.row].name |
| | | return cell |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | return listenNewModel.subjectList[page].count |
| | | } |
| | | |
| | | func numberOfSections(in collectionView: UICollectionView) -> Int { |
| | | return 1 |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_2_VC:VoicePlayerDelegate{ |
| | | func playing() { |
| | | view.isUserInteractionEnabled = false |
| | | print("正在播放") |
| | | //正在播放中,其他播放按钮先禁止 |
| | | for sub in stackView.arrangedSubviews as! [StudyHandleView]{ |
| | | sub.btn_pay.isEnabled = false |
| | | sub.btn_choose.isEnabled = false |
| | | } |
| | | } |
| | | |
| | | func playComplete() { |
| | | view.isUserInteractionEnabled = true |
| | | //对已经播放过的View,进行刷新 |
| | | for sub in stackView.arrangedSubviews as! [StudyHandleView]{ |
| | | sub.btn_pay.isEnabled = true |
| | | sub.btn_choose.isEnabled = true |
| | | if playedIndex.contains(sub.tag){ |
| | | sub.resetView() |
| | | sub.view_choose.alpha = playedIndex.contains(sub.tag) ? 1:0 |
| | | |
| | | } |
| | | } |
| | | |
| | | |
| | | for sub in view.subviews{ |
| | | if let v = sub as? StudyHandleView{ |
| | | v.resetView() |
| | | } |
| | | } |
| | | |
| | | |
| | | if viewModel.answerType.value == .success{ |
| | | let v = rootViewModel.answerCount.value |
| | | rootViewModel.answerCount.accept(v + 1) |
| | | viewModel.answerType.accept(.none) |
| | | |
| | | for sub in stackView.arrangedSubviews as! [StudyHandleView]{ |
| | | sub.btn_pay.isEnabled = false |
| | | sub.btn_choose.isEnabled = false |
| | | } |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | |
| | | |
| | | self.resetStackView() |
| | | } |
| | | } |
| | | |
| | | } |
| | | func playing() { |
| | | view.isUserInteractionEnabled = false |
| | | print("正在播放") |
| | | //正在播放中,其他播放按钮先禁止 |
| | | for sub in stackView.arrangedSubviews as! [StudyHandleView]{ |
| | | sub.btn_pay.isEnabled = false |
| | | sub.btn_choose.isEnabled = false |
| | | } |
| | | } |
| | | |
| | | func playComplete() { |
| | | collectionView.reloadData() |
| | | view.isUserInteractionEnabled = true |
| | | //对已经播放过的View,进行刷新 |
| | | for sub in stackView.arrangedSubviews as! [StudyHandleView]{ |
| | | sub.btn_pay.isEnabled = true |
| | | sub.btn_choose.isEnabled = true |
| | | if playedIndex.contains(sub.tag){ |
| | | sub.resetView() |
| | | sub.view_choose.alpha = playedIndex.contains(sub.tag) ? 1:0 |
| | | } |
| | | } |
| | | |
| | | |
| | | for sub in view.subviews{ |
| | | if let v = sub as? StudyHandleView{ |
| | | v.resetView() |
| | | } |
| | | } |
| | | |
| | | for v in tempViews{ |
| | | if v.isplayend{ |
| | | v.resetView() |
| | | } |
| | | } |
| | | |
| | | for (i,v) in tempViews.enumerated(){ |
| | | if v.isplayend == false{ |
| | | self.playedIndex.insert(i) |
| | | v.isplaying() |
| | | voicePlayer.playerAt(url: v.vioceSoundUrl) |
| | | break |
| | | } |
| | | } |
| | | |
| | | |
| | | if viewModel.answerType.value == .success{ |
| | | let v = rootViewModel.answerCount.value |
| | | rootViewModel.answerCount.accept(v + 1) |
| | | viewModel.answerType.accept(.none) |
| | | |
| | | for sub in stackView.arrangedSubviews as! [StudyHandleView]{ |
| | | sub.btn_pay.isEnabled = false |
| | | sub.btn_choose.isEnabled = false |
| | | } |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | self.resetStackView() |
| | | } |
| | | } |
| | | |
| | | } |
| | | } |
| | |
| | | |
| | | class HomeListenFight_lesson_3_VC: BaseVC { |
| | | |
| | | private var viewModel = FightAnswerViewModel() |
| | | private var viewModel = FightAnswerViewModel() |
| | | |
| | | private lazy var collectionView:UICollectionView = { |
| | | let flowLayout = UICollectionViewFlowLayout() |
| | | let w = (JQ_ScreenW - 97 - 54 - 36) / 3.0 |
| | | flowLayout.itemSize = CGSize(width: w, height: w * 0.947) |
| | | flowLayout.minimumInteritemSpacing = 18 |
| | | flowLayout.scrollDirection = .vertical |
| | | let collection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) |
| | | collection.register(UINib(nibName: "ListenFight_lesson_3_CCell", bundle: nil), forCellWithReuseIdentifier: "_ListenFight_lesson_3_CCell") |
| | | collection.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header") |
| | | collection.isScrollEnabled = false |
| | | return collection |
| | | }() |
| | | private lazy var collectionView:UICollectionView = { |
| | | let flowLayout = UICollectionViewFlowLayout() |
| | | let w = (JQ_ScreenW - 97 - 54 - 36) / 3.0 |
| | | flowLayout.itemSize = CGSize(width: w, height: w * 0.947) |
| | | flowLayout.minimumInteritemSpacing = 18 |
| | | flowLayout.scrollDirection = .vertical |
| | | let collection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) |
| | | collection.register(UINib(nibName: "ListenFight_lesson_3_CCell", bundle: nil), forCellWithReuseIdentifier: "_ListenFight_lesson_3_CCell") |
| | | collection.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header") |
| | | collection.isScrollEnabled = false |
| | | return collection |
| | | }() |
| | | |
| | | private lazy var stackView:UIStackView = { |
| | | let sta = UIStackView() |
| | | sta.spacing = 5 |
| | | sta.distribution = .equalSpacing |
| | | sta.axis = .horizontal |
| | | return sta |
| | | }() |
| | | private lazy var stackView:UIStackView = { |
| | | let sta = UIStackView() |
| | | sta.spacing = 5 |
| | | sta.distribution = .equalSpacing |
| | | sta.axis = .horizontal |
| | | return sta |
| | | }() |
| | | |
| | | private var listenNewModel:ListenNewModel! |
| | | private var page:Int! |
| | | private var answterCount:Int = 0 //回答计数,用于确定角标 |
| | | var rootViewModel:HomeListenFightViewModel! |
| | | var teamScheduleModel:TeamScheduleModel? |
| | | private var voicePlayer = VoicePlayer.share() |
| | | private var playIndex = Set<IndexPath>() //顺序播放 |
| | | private var isPlayingIndex:IndexPath? //正在播放中 |
| | | private var islisten:Bool = false |
| | | private var listenNewModel:ListenNewModel! |
| | | private var page:Int! |
| | | private var answterCount:Int = 0 //回答计数,用于确定角标 |
| | | var rootViewModel:HomeListenFightViewModel! |
| | | var teamScheduleModel:TeamScheduleModel? |
| | | private var voicePlayer = VoicePlayer.share() |
| | | private var playIndex = Set<IndexPath>() //顺序播放 |
| | | private var currentPlayIndex = IndexPath(row: 0, section: 0) |
| | | private var isPlayingIndex:IndexPath? //正在播放中 |
| | | private var islisten:Bool = false |
| | | private var handleClouse:(()->Void)? |
| | | |
| | | required init(page:Int,listenNewModel:ListenNewModel){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.page = page |
| | | self.listenNewModel = listenNewModel |
| | | } |
| | | required init(page:Int,listenNewModel:ListenNewModel){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.page = page |
| | | self.listenNewModel = listenNewModel |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | super.viewDidAppear(animated) |
| | | voicePlayer.delegate = self |
| | | } |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | super.viewDidAppear(animated) |
| | | voicePlayer.delegate = self |
| | | } |
| | | |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | voicePlayer.delegate = nil |
| | | VoicePlayer.share().playerInterrupt() |
| | | } |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | voicePlayer.delegate = nil |
| | | VoicePlayer.share().playerInterrupt() |
| | | } |
| | | |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | navigationItem.titleView = UIView() |
| | | playIndex.insert(IndexPath(row: 0, section: 0)) |
| | | listenNewModel.subjectList[page][0].isQuestion = 1 |
| | | listenNewModel.subjectList[page][1].isQuestion = 1 |
| | | listenNewModel.subjectList[page][3].isQuestion = 1 |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | navigationItem.titleView = UIView() |
| | | playIndex.insert(IndexPath(row: 0, section: 0)) |
| | | listenNewModel.subjectList[page][0].isQuestion = 1 |
| | | listenNewModel.subjectList[page][1].isQuestion = 1 |
| | | listenNewModel.subjectList[page][3].isQuestion = 1 |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+1) { |
| | | self.isPlayingIndex = IndexPath(row: 0, section: 0) |
| | | self.voicePlayer.playerAt(url: self.listenNewModel.subjectList[self.page][self.currentPlayIndex.row].correct) |
| | | self.collectionView.reloadData() |
| | | } |
| | | |
| | | |
| | | //回传记录,始终保持答题进度 |
| | | if let team = teamScheduleModel{ |
| | | for teamId in team.teamIds{ |
| | | for v in listenNewModel.subjectList[page]{ |
| | | if team.topicIds.contains(v.id){ |
| | | rootViewModel.insertCorrectAnswer(teamId: "\(teamId)", answerId: v.id) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | //回传记录,始终保持答题进度 |
| | | if let team = teamScheduleModel{ |
| | | for teamId in team.teamIds{ |
| | | for v in listenNewModel.subjectList[page]{ |
| | | if team.topicIds.contains(v.id){ |
| | | rootViewModel.insertCorrectAnswer(teamId: "\(teamId)", answerId: v.id) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | func restore(){ |
| | | playIndex.removeAll() |
| | | playIndex.insert(IndexPath(row: 0, section: 0)) |
| | | answterCount = 0 |
| | | for subView in view.subviews{ |
| | | if subView is Lesson_3_AnswerView{ |
| | | subView.removeFromSuperview() |
| | | } |
| | | } |
| | | setUI() |
| | | collectionView.reloadData() |
| | | func restore(){ |
| | | playIndex.removeAll() |
| | | playIndex.insert(IndexPath(row: 0, section: 0)) |
| | | answterCount = 0 |
| | | for subView in view.subviews{ |
| | | if subView is Lesson_3_AnswerView{ |
| | | subView.removeFromSuperview() |
| | | } |
| | | } |
| | | setUI() |
| | | collectionView.reloadData() |
| | | |
| | | } |
| | | } |
| | | |
| | | override func setUI() { |
| | | super.setUI() |
| | | if !view.subviews.contains(collectionView){ |
| | | collectionView.delegate = self |
| | | collectionView.dataSource = self |
| | | collectionView.showsVerticalScrollIndicator = false |
| | | collectionView.contentInset = UIEdgeInsets(top: 33, left: 0, bottom: 0, right: 0) |
| | | collectionView.backgroundColor = .clear |
| | | view.addSubview(collectionView) |
| | | } |
| | | override func setUI() { |
| | | super.setUI() |
| | | if !view.subviews.contains(collectionView){ |
| | | collectionView.delegate = self |
| | | collectionView.dataSource = self |
| | | collectionView.showsVerticalScrollIndicator = false |
| | | collectionView.contentInset = UIEdgeInsets(top: 33, left: 0, bottom: 0, right: 0) |
| | | collectionView.backgroundColor = .clear |
| | | view.addSubview(collectionView) |
| | | } |
| | | |
| | | collectionView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(0) |
| | | make.left.equalTo(97) |
| | | make.right.equalTo(-54) |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | } |
| | | collectionView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(0) |
| | | make.left.equalTo(97) |
| | | make.right.equalTo(-54) |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | } |
| | | |
| | | override func setRx() { |
| | | override func setRx() { |
| | | NotificationCenter.default.rx.notification(ResetLession_Noti).take(until: self.rx.deallocated).subscribe(onNext: {[weak self]_ in |
| | | self?.restore() |
| | | self?.viewDidLoad() |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | } |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | | let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | | let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | let w = (collectionView.size.width - flowLayout.minimumLineSpacing) / 3.1 |
| | | let h = (collectionView.size.height - flowLayout.minimumInteritemSpacing) / 2 - 40 |
| | | if flowLayout.itemSize.width != w || flowLayout.itemSize.height != h{ |
| | | flowLayout.itemSize = CGSize(width: w, height: h) |
| | | collectionView.reloadData() |
| | | } |
| | | } |
| | | |
| | | let w = (collectionView.size.width - flowLayout.minimumLineSpacing) / 3.1 |
| | | let h = (collectionView.size.height - flowLayout.minimumInteritemSpacing) / 2 - 40 |
| | | if flowLayout.itemSize.width != w || flowLayout.itemSize.height != h{ |
| | | flowLayout.itemSize = CGSize(width: w, height: h) |
| | | collectionView.reloadData() |
| | | } |
| | | } |
| | | |
| | | private func setAnswerStackView(force:Bool = false){ |
| | | private func setAnswerStackView(force:Bool = false){ |
| | | |
| | | |
| | | for v in stackView.arrangedSubviews{ |
| | | v.removeFromSuperview() |
| | | } |
| | | for v in stackView.arrangedSubviews{ |
| | | v.removeFromSuperview() |
| | | } |
| | | |
| | | var tempImageArray = [String]() |
| | | tempImageArray.append(listenNewModel.subjectList[page][2].img) |
| | | tempImageArray.append(listenNewModel.subjectList[page][4].img) |
| | | tempImageArray.append(listenNewModel.subjectList[page][5].img) |
| | | var tempImageArray = [String]() |
| | | tempImageArray.append(listenNewModel.subjectList[page][2].img) |
| | | tempImageArray.append(listenNewModel.subjectList[page][4].img) |
| | | tempImageArray.append(listenNewModel.subjectList[page][5].img) |
| | | |
| | | view.addSubview(stackView) |
| | | stackView.snp.makeConstraints { make in |
| | | make.right.equalToSuperview().offset(-82) |
| | | make.centerY.equalToSuperview().offset(10) |
| | | make.height.equalTo(52) |
| | | } |
| | | view.addSubview(stackView) |
| | | stackView.snp.makeConstraints { make in |
| | | make.right.equalToSuperview().offset(-82) |
| | | make.centerY.equalToSuperview().offset(10) |
| | | make.height.equalTo(52) |
| | | } |
| | | |
| | | var tempAnswerViews = [Lesson_3_AnswerView]() |
| | | for i in 0...2{ |
| | | let answerView = Lesson_3_AnswerView.jq_loadNibView() |
| | | answerView.alpha = 0 |
| | | answerView.btn_choose.addTarget(self, action: #selector(chooseAnswerAction), for: .touchUpInside) |
| | | answerView.btn_fullscreen.addTarget(self, action: #selector(fullscreenAction), for: .touchUpInside) |
| | | answerView.img_cover.contentMode = .scaleToFill |
| | | answerView.btn_choose.tag = 10+i |
| | | answerView.btn_fullscreen.tag = 20+i |
| | | answerView.imageUrl = tempImageArray[i] |
| | | answerView.img_cover.sd_setImage(with: URL(string: tempImageArray[i])) |
| | | answerView.snp.makeConstraints { make in |
| | | make.width.equalTo(85) |
| | | make.height.equalTo(52) |
| | | } |
| | | var tempAnswerViews = [Lesson_3_AnswerView]() |
| | | for i in 0...2{ |
| | | let answerView = Lesson_3_AnswerView.jq_loadNibView() |
| | | answerView.alpha = 0 |
| | | answerView.btn_choose.addTarget(self, action: #selector(chooseAnswerAction), for: .touchUpInside) |
| | | answerView.btn_fullscreen.addTarget(self, action: #selector(fullscreenAction), for: .touchUpInside) |
| | | answerView.img_cover.contentMode = .scaleToFill |
| | | answerView.btn_choose.tag = 10+i |
| | | answerView.btn_fullscreen.tag = 20+i |
| | | answerView.imageUrl = tempImageArray[i] |
| | | answerView.img_cover.sd_setImage(with: URL(string: tempImageArray[i])) |
| | | answerView.snp.makeConstraints { make in |
| | | make.width.equalTo(85) |
| | | make.height.equalTo(52) |
| | | } |
| | | |
| | | UIView.animate(withDuration: 0.05 + Double(i)) { |
| | | answerView.alpha = 1 |
| | | } |
| | | tempAnswerViews.append(answerView) |
| | | } |
| | | tempAnswerViews.shuffle() |
| | | stackView.addArrangedSubviews(tempAnswerViews) |
| | | } |
| | | // UIView.animate(withDuration: 0.05 + Double(i)) { |
| | | answerView.alpha = 1 |
| | | // } |
| | | tempAnswerViews.append(answerView) |
| | | } |
| | | tempAnswerViews.shuffle() |
| | | stackView.addArrangedSubviews(tempAnswerViews) |
| | | } |
| | | |
| | | @objc private func chooseAnswerAction(btn:UIButton){ |
| | | func handleClouseAction(clouse:@escaping ()->Void){ |
| | | self.handleClouse = clouse |
| | | } |
| | | |
| | | guard viewModel.selectIndex.value != nil else {return} |
| | | @objc private func chooseAnswerAction(btn:UIButton){ |
| | | |
| | | if !islisten{ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | guard viewModel.selectIndex.value != nil else {return} |
| | | |
| | | if answterCount == 0 && !playIndex.contains(IndexPath(row: 2, section: 0)){ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | if !islisten{ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | |
| | | if answterCount == 1 && !playIndex.contains(IndexPath(row: 1, section: 1)){ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | if answterCount == 0 && !playIndex.contains(IndexPath(row: 2, section: 0)){ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | |
| | | if answterCount == 2 && !playIndex.contains(IndexPath(row: 2, section: 1)){ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | if answterCount == 1 && !playIndex.contains(IndexPath(row: 1, section: 1)){ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | |
| | | var subV:Lesson_3_AnswerView? |
| | | if answterCount == 2 && !playIndex.contains(IndexPath(row: 2, section: 1)){ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | |
| | | for (_,v) in (stackView.arrangedSubviews as! [Lesson_3_AnswerView]).enumerated(){ |
| | | if v.btn_choose.tag == btn.tag{ |
| | | subV = v;break |
| | | } |
| | | } |
| | | var subV:Lesson_3_AnswerView? |
| | | |
| | | var answerType:Fight_lessonType = .none |
| | | for (_,v) in (stackView.arrangedSubviews as! [Lesson_3_AnswerView]).enumerated(){ |
| | | if v.btn_choose.tag == btn.tag{ |
| | | subV = v;break |
| | | } |
| | | } |
| | | |
| | | var valueIndex:Int! //图片的角标 |
| | | if viewModel.selectIndex.value?.section == 0{ |
| | | valueIndex = viewModel.selectIndex.value!.row |
| | | } |
| | | if viewModel.selectIndex.value?.section == 1{ |
| | | valueIndex = viewModel.selectIndex.value!.row + 3 |
| | | } |
| | | var answerType:Fight_lessonType = .none |
| | | |
| | | if subV?.imageUrl == listenNewModel.subjectList[page][valueIndex].img{ |
| | | listenNewModel.subjectList[page][valueIndex].isAnster = true |
| | | answerType = .success |
| | | subV?.alpha = 0 |
| | | voicePlayer.playSuccessVoice() |
| | | }else{ |
| | | answerType = .fail |
| | | voicePlayer.playFailVoice() |
| | | } |
| | | var valueIndex:Int! //图片的角标 |
| | | if viewModel.selectIndex.value?.section == 0{ |
| | | valueIndex = viewModel.selectIndex.value!.row |
| | | } |
| | | if viewModel.selectIndex.value?.section == 1{ |
| | | valueIndex = viewModel.selectIndex.value!.row + 3 |
| | | } |
| | | |
| | | switch answerType { |
| | | case .success: |
| | | rootViewModel.correctNum += 1 |
| | | viewModel.answerType.accept(.success) |
| | | let copyViewFrame = subV?.convert(subV!.bounds, to: self.view) |
| | | let copyView = subV?.copyView() |
| | | copyView?.frame = copyViewFrame! |
| | | copyView?.img_cover.contentMode = .scaleToFill |
| | | copyView?.img_cover.image = subV?.img_cover.image |
| | | self.view.addSubview(copyView!) |
| | | copyView?.layoutIfNeeded() |
| | | if subV?.imageUrl == listenNewModel.subjectList[page][valueIndex].img{ |
| | | listenNewModel.subjectList[page][valueIndex].isAnster = true |
| | | answerType = .success |
| | | subV?.alpha = 0 |
| | | voicePlayer.playSuccessVoice() |
| | | currentPlayIndex = IndexPath(row: 0, section: 1) |
| | | self.voicePlayer.playerAt(url: self.listenNewModel.subjectList[self.page][currentPlayIndex.row + 3].correct) |
| | | |
| | | var ansterIndePath:IndexPath? |
| | | if viewModel.selectIndex.value?.section == 0{ |
| | | ansterIndePath = IndexPath(row: 2, section: 0) |
| | | playIndex.insert(IndexPath(row: 0, section: 1)) //下一个准备播放 |
| | | } |
| | | let model = listenNewModel.list[self.page] |
| | | model.status = 2 |
| | | Services.answerQuestion(id: model.id, status: 2).subscribe(onNext: {_ in |
| | | self.handleClouse?() |
| | | }).disposed(by: self.disposeBag) |
| | | |
| | | if viewModel.selectIndex.value?.section == 1 && (viewModel.selectIndex.value?.row == 0 || viewModel.selectIndex.value?.row == 1){ |
| | | ansterIndePath = IndexPath(row: 1, section: 1) |
| | | playIndex.insert(IndexPath(row: 2, section: 1)) //下一个准备播放 |
| | | } |
| | | }else{ |
| | | answerType = .fail |
| | | voicePlayer.playFailVoice() |
| | | |
| | | if viewModel.selectIndex.value?.section == 1 && viewModel.selectIndex.value?.row == 2{ |
| | | ansterIndePath = IndexPath(row: 2, section: 1) |
| | | } |
| | | let model = listenNewModel.list[self.page] |
| | | model.status = 3 |
| | | Services.answerQuestion(id: model.id, status: 3).subscribe(onNext: {_ in |
| | | self.handleClouse?() |
| | | }).disposed(by: self.disposeBag) |
| | | } |
| | | |
| | | guard ansterIndePath != nil else {return} |
| | | switch answerType { |
| | | case .success: |
| | | rootViewModel.correctNum += 1 |
| | | viewModel.answerType.accept(.success) |
| | | let copyViewFrame = subV?.convert(subV!.bounds, to: self.view) |
| | | let copyView = subV?.copyView() |
| | | copyView?.frame = copyViewFrame! |
| | | copyView?.img_cover.contentMode = .scaleToFill |
| | | copyView?.img_cover.image = subV?.img_cover.image |
| | | self.view.addSubview(copyView!) |
| | | copyView?.layoutIfNeeded() |
| | | |
| | | if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_3_CCell", for: ansterIndePath!) as? ListenFight_lesson_3_CCell{ |
| | | var newFrame = cell.img_cover.convert(cell.img_cover.bounds, to: self.view) |
| | | newFrame.origin.x += 0 |
| | | newFrame.origin.y += 0 |
| | | var ansterIndePath:IndexPath? |
| | | if viewModel.selectIndex.value?.section == 0{ |
| | | ansterIndePath = IndexPath(row: 2, section: 0) |
| | | playIndex.insert(IndexPath(row: 0, section: 1)) //下一个准备播放 |
| | | } |
| | | |
| | | let successImage = UIImageView(image: UIImage(named: "icon_success")) |
| | | successImage.bounds = CGRect(x: 0, y: 0, width: 75, height: 75) |
| | | successImage.center = newFrame.center |
| | | successImage.transform = .init(scaleX: 0.1, y: 0.1) |
| | | successImage.alpha = 0 |
| | | successImage.layoutIfNeeded() |
| | | self.view.addSubview(successImage) |
| | | UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.4) { |
| | | successImage.transform = .init(scaleX: 1.0, y: 1.0) |
| | | successImage.alpha = 1 |
| | | } |
| | | if viewModel.selectIndex.value?.section == 1 && (viewModel.selectIndex.value?.row == 0 || viewModel.selectIndex.value?.row == 1){ |
| | | ansterIndePath = IndexPath(row: 1, section: 1) |
| | | playIndex.insert(IndexPath(row: 2, section: 1)) //下一个准备播放 |
| | | } |
| | | |
| | | UIView.animate(withDuration: 0.5, delay: 3) { |
| | | successImage.transform = .init(scaleX: 0.1, y: 0.1) |
| | | successImage.alpha = 0 |
| | | }completion: { _ in |
| | | successImage.removeFromSuperview() |
| | | } |
| | | if viewModel.selectIndex.value?.section == 1 && viewModel.selectIndex.value?.row == 2{ |
| | | ansterIndePath = IndexPath(row: 2, section: 1) |
| | | } |
| | | |
| | | UIView.animate(withDuration: 0.4) { |
| | | copyView?.frame = newFrame |
| | | } completion: { _ in |
| | | self.answterCount += 1 |
| | | guard ansterIndePath != nil else {return} |
| | | |
| | | let cell = self.collectionView.cellForItem(at: self.viewModel.selectIndex.value!) as! ListenFight_lesson_3_CCell |
| | | self.isPlayingIndex = self.viewModel.selectIndex.value |
| | | cell.isPlaying(isplaying: true) |
| | | if let cell = collectionView.cellForItem(at: ansterIndePath!) as? ListenFight_lesson_3_CCell{ |
| | | var newFrame = cell.img_cover.convert(cell.img_cover.bounds, to: self.view) |
| | | newFrame.origin.x += 0 |
| | | newFrame.origin.y += 0 |
| | | |
| | | self.voicePlayer.playerAt(url: self.listenNewModel.subjectList[self.page][valueIndex].correct) |
| | | let successImage = UIImageView(image: UIImage(named: "icon_success")) |
| | | successImage.bounds = CGRect(x: 0, y: 0, width: 75, height: 75) |
| | | successImage.center = newFrame.center |
| | | successImage.transform = .init(scaleX: 0.1, y: 0.1) |
| | | successImage.alpha = 0 |
| | | successImage.layoutIfNeeded() |
| | | self.view.addSubview(successImage) |
| | | UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.4) { |
| | | successImage.transform = .init(scaleX: 1.0, y: 1.0) |
| | | successImage.alpha = 1 |
| | | } |
| | | |
| | | let teamId = self.listenNewModel.data?.id.components(separatedBy: ",")[self.page] |
| | | let answerId = self.listenNewModel.subjectList[self.page][valueIndex].id |
| | | self.rootViewModel.insertCorrectAnswer(teamId: teamId, answerId: answerId) |
| | | UIView.animate(withDuration: 0.5, delay: 3) { |
| | | successImage.transform = .init(scaleX: 0.1, y: 0.1) |
| | | successImage.alpha = 0 |
| | | }completion: { _ in |
| | | successImage.removeFromSuperview() |
| | | } |
| | | |
| | | for v in self.stackView.arrangedSubviews{ |
| | | v.removeFromSuperview() |
| | | } |
| | | UIView.animate(withDuration: 0.4) { |
| | | copyView?.frame = newFrame |
| | | } completion: { _ in |
| | | self.answterCount += 1 |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+1.0) { |
| | | self.viewModel.selectIndex.accept(nil) |
| | | let v = self.rootViewModel.answerCount.value + 1 |
| | | self.rootViewModel.answerCount.accept(v) |
| | | self.collectionView.reloadData() |
| | | } |
| | | } |
| | | } |
| | | let cell = self.collectionView.cellForItem(at: self.viewModel.selectIndex.value!) as! ListenFight_lesson_3_CCell |
| | | self.isPlayingIndex = self.viewModel.selectIndex.value |
| | | cell.isPlaying(isplaying: true) |
| | | |
| | | case .fail: |
| | | rootViewModel.errorNum += 1 |
| | | viewModel.answerType.accept(.fail) |
| | | UIView.animate(withDuration: 0.4) { |
| | | subV?.img_state.alpha = 1 |
| | | subV?.btn_fullscreen.alpha = 0 |
| | | }completion: { _ in |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+1.5) { |
| | | self.islisten = false |
| | | for v in self.stackView.arrangedSubviews{ |
| | | v.removeFromSuperview() |
| | | } |
| | | } |
| | | } |
| | | default:break |
| | | } |
| | | } |
| | | self.voicePlayer.playerAt(url: self.listenNewModel.subjectList[self.page][valueIndex].correct) |
| | | |
| | | @objc private func fullscreenAction(btn:UIButton){ |
| | | |
| | | var answerView:Lesson_3_AnswerView! |
| | | for (_,v) in (stackView.arrangedSubviews as! [Lesson_3_AnswerView]).enumerated(){ |
| | | if v.btn_fullscreen.tag == btn.tag{ |
| | | answerView = v;break |
| | | } |
| | | } |
| | | let teamId = self.listenNewModel.data?.id.components(separatedBy: ",")[self.page] |
| | | let answerId = self.listenNewModel.subjectList[self.page][valueIndex].id |
| | | self.rootViewModel.insertCorrectAnswer(teamId: teamId, answerId: answerId) |
| | | |
| | | for v in self.stackView.arrangedSubviews{ |
| | | v.removeFromSuperview() |
| | | } |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+1.0) { |
| | | // self.viewModel.selectIndex.accept(nil) |
| | | let v = self.rootViewModel.answerCount.value + 1 |
| | | self.rootViewModel.answerCount.accept(v) |
| | | self.collectionView.reloadData() |
| | | } |
| | | } |
| | | } |
| | | |
| | | case .fail: |
| | | rootViewModel.errorNum += 1 |
| | | viewModel.answerType.accept(.fail) |
| | | UIView.animate(withDuration: 0.4) { |
| | | subV?.img_state.alpha = 1 |
| | | subV?.btn_fullscreen.alpha = 0 |
| | | }completion: { _ in |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+1.5) { |
| | | self.islisten = false |
| | | for v in self.stackView.arrangedSubviews{ |
| | | v.removeFromSuperview() |
| | | } |
| | | } |
| | | } |
| | | default:break |
| | | } |
| | | } |
| | | |
| | | @objc private func fullscreenAction(btn:UIButton){ |
| | | |
| | | var answerView:Lesson_3_AnswerView! |
| | | for (_,v) in (stackView.arrangedSubviews as! [Lesson_3_AnswerView]).enumerated(){ |
| | | if v.btn_fullscreen.tag == btn.tag{ |
| | | answerView = v;break |
| | | } |
| | | } |
| | | |
| | | |
| | | let lantern = Lantern() |
| | | lantern.numberOfItems = { () in |
| | | return 1 |
| | | } |
| | | let lantern = Lantern() |
| | | lantern.numberOfItems = { () in |
| | | return 1 |
| | | } |
| | | |
| | | lantern.cellClassAtIndex = { _ in |
| | | LanternImageCell.self |
| | | } |
| | | lantern.cellClassAtIndex = { _ in |
| | | LanternImageCell.self |
| | | } |
| | | |
| | | |
| | | lantern.transitionAnimator = LanternZoomAnimator(previousView: { index -> UIView? in |
| | | return answerView.img_cover |
| | | }) |
| | | lantern.transitionAnimator = LanternZoomAnimator(previousView: { index -> UIView? in |
| | | return answerView.img_cover |
| | | }) |
| | | |
| | | |
| | | // UIPageIndicator样式的页码指示器 |
| | | lantern.pageIndicator = LanternDefaultPageIndicator() |
| | | // UIPageIndicator样式的页码指示器 |
| | | lantern.pageIndicator = LanternDefaultPageIndicator() |
| | | |
| | | lantern.pageIndex = 0 |
| | | lantern.pageIndex = 0 |
| | | |
| | | lantern.reloadCellAtIndex = { context in |
| | | let lanternCell = context.cell as? LanternImageCell |
| | | lanternCell?.imageView.image = answerView.img_cover.image |
| | | } |
| | | //不要使用push |
| | | lantern.show() |
| | | } |
| | | lantern.reloadCellAtIndex = { context in |
| | | let lanternCell = context.cell as? LanternImageCell |
| | | lanternCell?.imageView.image = answerView.img_cover.image |
| | | } |
| | | //不要使用push |
| | | lantern.show() |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_3_VC:UICollectionViewDelegate{ |
| | | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
| | | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
| | | |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_3_VC:UICollectionViewDelegateFlowLayout{ |
| | | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { |
| | | if section == 0{ |
| | | return CGSize.zero |
| | | } |
| | | return CGSizeMake(JQ_ScreenW, 65) |
| | | } |
| | | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { |
| | | if section == 0{ |
| | | return CGSize.zero |
| | | } |
| | | return CGSizeMake(JQ_ScreenW, 65) |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_3_VC:UICollectionViewDataSource{ |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_3_CCell", for: indexPath) as! ListenFight_lesson_3_CCell |
| | | cell.backgroundColor = .clear |
| | | cell.indexPath = indexPath |
| | | cell.contentView.backgroundColor = .clear |
| | | cell.canClick(playIndex.contains(indexPath)) |
| | | cell.palyVoiceAt {[weak self] index in |
| | | guard let weakSelf = self else { return } |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_3_CCell", for: indexPath) as! ListenFight_lesson_3_CCell |
| | | cell.indexPath = indexPath |
| | | cell.canClick(playIndex.contains(indexPath)) |
| | | cell.palyVoiceAt {[weak self] index in |
| | | guard let weakSelf = self else { return } |
| | | |
| | | weakSelf.isPlayingIndex = index |
| | | weakSelf.viewModel.selectIndex.accept(index) |
| | | weakSelf.isPlayingIndex = index |
| | | weakSelf.viewModel.selectIndex.accept(index) |
| | | |
| | | weakSelf.voicePlayer.playerEnd() |
| | | weakSelf.voicePlayer.playerEnd() |
| | | |
| | | if indexPath.section == 1{ |
| | | weakSelf.voicePlayer.playerAt(url: weakSelf.listenNewModel.subjectList[weakSelf.page][indexPath.row + 3].correct) |
| | | }else{ |
| | | weakSelf.voicePlayer.playerAt(url: weakSelf.listenNewModel.subjectList[weakSelf.page][indexPath.row].correct) |
| | | } |
| | | if indexPath.section == 1{ |
| | | weakSelf.voicePlayer.playerAt(url: weakSelf.listenNewModel.subjectList[weakSelf.page][indexPath.row + 3].correct) |
| | | }else{ |
| | | weakSelf.voicePlayer.playerAt(url: weakSelf.listenNewModel.subjectList[weakSelf.page][indexPath.row].correct) |
| | | } |
| | | |
| | | |
| | | if index == IndexPath(row: 2, section: 0) || index == IndexPath(row: 1, section: 1) || index == IndexPath(row: 2, section: 1){ |
| | | if index == IndexPath(row: 2, section: 0) || index == IndexPath(row: 1, section: 1) || index == IndexPath(row: 2, section: 1){ |
| | | |
| | | var model:Listen1SubModel? |
| | | if indexPath.section == 0{ |
| | | model = weakSelf.listenNewModel.subjectList[weakSelf.page][indexPath.row] |
| | | } |
| | | var model:Listen1SubModel? |
| | | if indexPath.section == 0{ |
| | | model = weakSelf.listenNewModel.subjectList[weakSelf.page][indexPath.row] |
| | | } |
| | | |
| | | if indexPath.section == 1{ |
| | | model = weakSelf.listenNewModel.subjectList[weakSelf.page][indexPath.row + 3] |
| | | } |
| | | if indexPath.section == 1{ |
| | | model = weakSelf.listenNewModel.subjectList[weakSelf.page][indexPath.row + 3] |
| | | } |
| | | |
| | | if model?.isAnster == false{ |
| | | //点击答案,就显示 |
| | | weakSelf.setAnswerStackView() |
| | | } |
| | | } |
| | | if model?.isAnster == false{ |
| | | //点击答案,就显示 |
| | | weakSelf.setAnswerStackView() |
| | | } |
| | | } |
| | | |
| | | collectionView.reloadItems(at: [index]) |
| | | } |
| | | collectionView.reloadItems(at: [index]) |
| | | } |
| | | |
| | | if indexPath.section == 0{ |
| | | let model = listenNewModel.subjectList[page][indexPath.row] |
| | | if indexPath.row != 2{ |
| | | cell.img_cover.sd_setImage(with: URL(string: model.img)) |
| | | }else{ |
| | | cell.img_cover.image = nil |
| | | } |
| | | cell.setModel(model,isplaying: isPlayingIndex == indexPath) |
| | | } |
| | | if indexPath.section == 0{ |
| | | let model = listenNewModel.subjectList[page][indexPath.row] |
| | | if indexPath.row != 2{ |
| | | cell.img_cover.sd_setImage(with: URL(string: model.img)) |
| | | }else{ |
| | | cell.img_cover.image = nil |
| | | } |
| | | cell.setModel(model,isplaying: isPlayingIndex == indexPath) |
| | | } |
| | | |
| | | if indexPath.section == 1{ |
| | | let model = listenNewModel.subjectList[page][indexPath.row + 3] |
| | | if indexPath.row == 0{ |
| | | cell.img_cover.sd_setImage(with: URL(string: model.img)) |
| | | }else{ |
| | | cell.img_cover.image = nil |
| | | } |
| | | cell.setModel(model,isplaying: isPlayingIndex == indexPath) |
| | | } |
| | | return cell |
| | | } |
| | | if indexPath.section == 1{ |
| | | let model = listenNewModel.subjectList[page][indexPath.row + 3] |
| | | if indexPath.row == 0{ |
| | | cell.img_cover.sd_setImage(with: URL(string: model.img)) |
| | | }else{ |
| | | cell.img_cover.image = nil |
| | | } |
| | | cell.setModel(model,isplaying: isPlayingIndex == indexPath) |
| | | } |
| | | return cell |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { |
| | | let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) |
| | | return reusableView |
| | | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { |
| | | let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) |
| | | return reusableView |
| | | |
| | | } |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | return 3 |
| | | } |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | return 3 |
| | | } |
| | | |
| | | func numberOfSections(in collectionView: UICollectionView) -> Int { |
| | | return 2 |
| | | } |
| | | func numberOfSections(in collectionView: UICollectionView) -> Int { |
| | | return 2 |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_3_VC:VoicePlayerDelegate{ |
| | | func playComplete() { |
| | | view.isUserInteractionEnabled = true |
| | | isPlayingIndex = nil |
| | | islisten = true |
| | | var nextRow = (viewModel.selectIndex.value?.row ?? 0) + 1 |
| | | var section = (viewModel.selectIndex.value?.section ?? 0) + 0 |
| | | func playComplete() { |
| | | view.isUserInteractionEnabled = true |
| | | isPlayingIndex = nil |
| | | islisten = true |
| | | var nextRow = (viewModel.selectIndex.value?.row ?? 0) + 1 |
| | | var section = (viewModel.selectIndex.value?.section ?? 0) + 0 |
| | | |
| | | if nextRow >= 3{ |
| | | nextRow = 0;section = 1 |
| | | } |
| | | if nextRow >= 3{ |
| | | nextRow = 0;section = 1 |
| | | } |
| | | |
| | | currentPlayIndex.row+=1 |
| | | if currentPlayIndex.row < 3{ |
| | | |
| | | if self.answterCount == 3{ |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: nil) |
| | | return |
| | | } |
| | | var row = currentPlayIndex.row |
| | | if currentPlayIndex.section == 1{ |
| | | row += 3 |
| | | } |
| | | |
| | | if (viewModel.selectIndex.value?.section == 0 && viewModel.selectIndex.value?.row == 2) || (viewModel.selectIndex.value?.section == 1 && viewModel.selectIndex.value?.row == 1){ |
| | | collectionView.reloadData() |
| | | return |
| | | } |
| | | playIndex.insert(IndexPath(row: nextRow, section: section)) //下一个准备播放 |
| | | collectionView.reloadData() |
| | | } |
| | | self.voicePlayer.playerAt(url: self.listenNewModel.subjectList[self.page][row].correct) |
| | | self.isPlayingIndex = currentPlayIndex |
| | | self.viewModel.selectIndex.accept(currentPlayIndex) |
| | | self.playIndex.insert(currentPlayIndex) |
| | | } |
| | | |
| | | func playing() { |
| | | islisten = false |
| | | view.isUserInteractionEnabled = false |
| | | } |
| | | if currentPlayIndex.row == 2{ |
| | | self.isPlayingIndex = currentPlayIndex |
| | | self.viewModel.selectIndex.accept(currentPlayIndex) |
| | | self.playIndex.insert(currentPlayIndex) |
| | | self.setAnswerStackView() |
| | | } |
| | | |
| | | if self.answterCount == 2{ |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: nil) |
| | | return |
| | | } |
| | | |
| | | if (viewModel.selectIndex.value?.section == 0 && viewModel.selectIndex.value?.row == 2) || (viewModel.selectIndex.value?.section == 1 && viewModel.selectIndex.value?.row == 1){ |
| | | collectionView.reloadData() |
| | | return |
| | | } |
| | | playIndex.insert(IndexPath(row: nextRow, section: section)) //下一个准备播放 |
| | | collectionView.reloadData() |
| | | } |
| | | |
| | | func playing() { |
| | | islisten = false |
| | | view.isUserInteractionEnabled = false |
| | | } |
| | | } |
| | |
| | | // |
| | | |
| | | import UIKit |
| | | import JQTools |
| | | |
| | | class HomeListenFight_lesson_4_VC: BaseVC { |
| | | |
| | | private var viewModel = FightAnswerViewModel() |
| | | private var listenNewModel:ListenNewModel! |
| | | private var page:Int! |
| | | var rootViewModel:HomeListenFightViewModel! |
| | | var teamScheduleModel:TeamScheduleModel? |
| | | |
| | | private var answerIndex:IndexPath? //答案的Index |
| | | private var answerIndexs = Set<IndexPath>() //回答过的Index集合 |
| | | private var filterItems = [[Listen1SubModel]]() //此类型特殊,需要数据清理 |
| | | |
| | | private lazy var stackView:UIStackView = { |
| | | let sta = UIStackView() |
| | | sta.spacing = 41 |
| | | sta.distribution = .equalSpacing |
| | | sta.axis = .vertical |
| | | return sta |
| | | }() |
| | | |
| | | private lazy var collectionView:UICollectionView = { |
| | | let flowLayout = UICollectionViewFlowLayout() |
| | | let w = (JQ_ScreenW - 164 * 2 - 62) / 2.0 |
| | | flowLayout.itemSize = CGSize(width: w, height: w * 0.842) |
| | | flowLayout.minimumInteritemSpacing = 0 |
| | | flowLayout.scrollDirection = .vertical |
| | | let collection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) |
| | | collection.contentInset = UIEdgeInsets(top: 33, left: 0, bottom: 0, right: 0) |
| | | collection.register(UINib(nibName: "ListenFight_lesson_4_CCell", bundle: nil), forCellWithReuseIdentifier: "_ListenFight_lesson_4_CCell") |
| | | collection.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header") |
| | | collection.isScrollEnabled = false |
| | | return collection |
| | | }() |
| | | |
| | | private var voicePlayer = VoicePlayer.share() |
| | | |
| | | required init(page:Int,listenNewModel:ListenNewModel){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.page = page |
| | | self.listenNewModel = listenNewModel |
| | | let temp1 = Array(listenNewModel.subjectList[page][0...1]) |
| | | let temp2 = Array(listenNewModel.subjectList[page][2...3]) |
| | | |
| | | filterItems.append(temp1) |
| | | filterItems.append(temp2) |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | |
| | | func restore(){ |
| | | answerIndexs.removeAll() |
| | | answerIndex = nil |
| | | for subView in view.subviews{ |
| | | if subView is Lesson_4_AnswerView{ |
| | | subView.removeFromSuperview() |
| | | } |
| | | } |
| | | |
| | | setUI() |
| | | collectionView.reloadData() |
| | | } |
| | | |
| | | |
| | | private var viewModel = FightAnswerViewModel() |
| | | private var listenNewModel:ListenNewModel! |
| | | private var page:Int! |
| | | var rootViewModel:HomeListenFightViewModel! |
| | | var teamScheduleModel:TeamScheduleModel? |
| | | |
| | | private var answerIndex:IndexPath? //答案的Index |
| | | private var answerIndexs = Set<IndexPath>() //回答过的Index集合 |
| | | private var filterItems = [[Listen1SubModel]]() //此类型特殊,需要数据清理 |
| | | private var handleClouse:(()->Void)? |
| | | |
| | | private lazy var stackView:UIStackView = { |
| | | let sta = UIStackView() |
| | | sta.spacing = 41 |
| | | sta.distribution = .equalSpacing |
| | | sta.axis = .vertical |
| | | return sta |
| | | }() |
| | | |
| | | private lazy var collectionView:UICollectionView = { |
| | | let flowLayout = UICollectionViewFlowLayout() |
| | | let w = (JQ_ScreenW - 164 * 2 - 62) / 2.0 |
| | | flowLayout.minimumLineSpacing = 20 |
| | | flowLayout.minimumInteritemSpacing = 0 |
| | | flowLayout.itemSize = CGSize(width: w, height: w * 0.702) |
| | | flowLayout.scrollDirection = .vertical |
| | | let collection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) |
| | | collection.contentInset = UIEdgeInsets(top: 33, left: 0, bottom: 0, right: 0) |
| | | collection.register(UINib(nibName: "ListenFight_lesson_4_CCell", bundle: nil), forCellWithReuseIdentifier: "_ListenFight_lesson_4_CCell") |
| | | collection.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header") |
| | | collection.isScrollEnabled = false |
| | | return collection |
| | | }() |
| | | |
| | | private var voicePlayer = VoicePlayer.share() |
| | | |
| | | required init(page:Int,listenNewModel:ListenNewModel){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.page = page |
| | | self.listenNewModel = listenNewModel |
| | | let temp1 = Array(listenNewModel.subjectList[page][0...1]) |
| | | let temp2 = Array(listenNewModel.subjectList[page][2...3]) |
| | | |
| | | filterItems.append(temp1) |
| | | filterItems.append(temp2) |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | |
| | | func restore(){ |
| | | answerIndexs.removeAll() |
| | | answerIndex = nil |
| | | for subView in view.subviews{ |
| | | if subView is Lesson_4_AnswerView{ |
| | | subView.removeFromSuperview() |
| | | } |
| | | } |
| | | |
| | | setUI() |
| | | collectionView.reloadData() |
| | | } |
| | | |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | |
| | | |
| | | //回传记录,始终保持答题进度 |
| | | if let team = teamScheduleModel{ |
| | | for teamId in team.teamIds{ |
| | | for v in listenNewModel.subjectList[page]{ |
| | | if team.topicIds.contains(v.id){ |
| | | rootViewModel.insertCorrectAnswer(teamId: "\(teamId)", answerId: v.id) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | //回传记录,始终保持答题进度 |
| | | if let team = teamScheduleModel{ |
| | | for teamId in team.teamIds{ |
| | | for v in listenNewModel.subjectList[page]{ |
| | | if team.topicIds.contains(v.id){ |
| | | rootViewModel.insertCorrectAnswer(teamId: "\(teamId)", answerId: v.id) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+1) { |
| | | var index:IndexPath! |
| | | if self.filterItems[0][0].isQuestion == 1{ |
| | | index = IndexPath(row: 0, section: 0) |
| | | }else{ |
| | | index = IndexPath(row: 1, section: 0) |
| | | } |
| | | |
| | | let m = self.filterItems[index.section][index.row] |
| | | self.viewModel.selectIndex.accept(index) |
| | | UIView.animate(withDuration: 0.5) { |
| | | self.collectionView.snp.remakeConstraints { make in |
| | | make.left.equalTo(76) |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(0) |
| | | make.width.equalTo(JQ_ScreenW - 164 * 2) |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | self.view.layoutIfNeeded() |
| | | }completion: { _ in |
| | | self.setAnswerStackView() |
| | | VoicePlayer.share().playerAt(url: m.correct) |
| | | } |
| | | } |
| | | |
| | | |
| | | } |
| | | |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | super.viewDidAppear(animated) |
| | | voicePlayer.delegate = self |
| | | } |
| | | |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | voicePlayer.delegate = nil |
| | | VoicePlayer.share().playerInterrupt() |
| | | } |
| | | |
| | | |
| | | override func setUI() { |
| | | super.setUI() |
| | | |
| | | if !view.subviews.contains(collectionView){ |
| | | collectionView.delegate = self |
| | | collectionView.dataSource = self |
| | | collectionView.showsVerticalScrollIndicator = false |
| | | collectionView.backgroundColor = .clear |
| | | view.addSubview(collectionView) |
| | | } |
| | | |
| | | collectionView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(0) |
| | | make.left.equalTo(164) |
| | | make.width.equalTo(JQ_ScreenW - 164 * 2) |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | } |
| | | |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | | let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | |
| | | let w = (collectionView.size.width - flowLayout.minimumLineSpacing) / 2 |
| | | let h = (collectionView.size.height - flowLayout.minimumInteritemSpacing) / 2.1 |
| | | |
| | | if flowLayout.itemSize.width != w || flowLayout.itemSize.height != h{ |
| | | flowLayout.itemSize = CGSize(width: w, height: h) |
| | | collectionView.reloadData() |
| | | } |
| | | } |
| | | |
| | | private func setAnswerStackView(){ |
| | | |
| | | guard let selectIndex = viewModel.selectIndex.value else{ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | |
| | | for v in stackView.subviews{ |
| | | v.removeFromSuperview() |
| | | } |
| | | |
| | | if !view.subviews.contains(stackView){ |
| | | view.addSubview(stackView) |
| | | stackView.snp.makeConstraints { make in |
| | | make.right.equalToSuperview().offset(-14) |
| | | make.centerY.equalToSuperview() |
| | | } |
| | | } |
| | | |
| | | let answerModel = filterItems[selectIndex.section].filter({$0.isQuestion == 0}).first! |
| | | let answerId = answerModel.id |
| | | |
| | | var tempVoiceArray = [String]() |
| | | for v in filterItems[selectIndex.section]{ |
| | | if v.id == answerId{ |
| | | tempVoiceArray.append(v.correct) |
| | | tempVoiceArray.append(v.error.components(separatedBy: ",").first ?? "") |
| | | tempVoiceArray.append(v.error.components(separatedBy: ",").last ?? "") |
| | | } |
| | | } |
| | | |
| | | tempVoiceArray.shuffle() |
| | | |
| | | |
| | | for i in 0...2{ |
| | | let answerView = Lesson_4_AnswerView.jq_loadNibView() |
| | | answerView.btn_choose.tag = 10 + i |
| | | answerView.tag = 20 + i |
| | | answerView.voiceUrl = tempVoiceArray[i] |
| | | answerView.btn_isAnswer.setImage(viewModel.selectIndex.value?.row == 1 ? UIImage(named: "icon_question"):UIImage(named: "icon_answer"), for: .normal) |
| | | answerView.btn_choose.addTarget(self, action: #selector(answerAction), for: .touchUpInside) |
| | | answerView.alpha = 0 |
| | | answerView.snp.makeConstraints { make in |
| | | make.width.equalTo(221) |
| | | make.height.equalTo(52) |
| | | } |
| | | answerView.btn_choose.isEnabled = false |
| | | answerView.playAt { index in |
| | | answerView.btn_choose.isEnabled = true |
| | | } |
| | | |
| | | UIView.animate(withDuration: 0.05 + Double(i)) { |
| | | answerView.alpha = 1 |
| | | } |
| | | |
| | | stackView.insertArrangedSubview(answerView, at: 0) |
| | | } |
| | | } |
| | | |
| | | @objc func answerAction(btn:UIButton){ |
| | | |
| | | var islistenDone:Int = 0 |
| | | for v in stackView.arrangedSubviews as! [Lesson_4_AnswerView]{ |
| | | if v.btn_choose.isEnabled == true{ |
| | | islistenDone += 1 |
| | | } |
| | | } |
| | | |
| | | if islistenDone != 3{ |
| | | alertError(msg: "请先听完");return |
| | | } |
| | | |
| | | guard let selectIndex = viewModel.selectIndex.value else{ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | |
| | | var tempSubV:Lesson_4_AnswerView? |
| | | |
| | | for subV in stackView.arrangedSubviews{ |
| | | if let s = subV as? Lesson_4_AnswerView{ |
| | | s.btn_choose.isSelected = btn.tag == s.btn_choose.tag |
| | | if s.btn_choose.isSelected{tempSubV = s} |
| | | } |
| | | } |
| | | |
| | | var answerModel:Listen1SubModel? |
| | | for (index,v) in filterItems[selectIndex.section].enumerated(){ |
| | | let m = filterItems[selectIndex.section].filter({$0.isQuestion == 0}).first |
| | | if v.id == m?.id{ |
| | | answerModel = v |
| | | answerIndex = IndexPath(row: index, section: viewModel.selectIndex.value!.section) |
| | | } |
| | | } |
| | | |
| | | var answerType:Fight_lessonType = .none |
| | | if tempSubV?.voiceUrl == answerModel?.correct{ |
| | | answerType = .success |
| | | voicePlayer.playSuccessVoice() |
| | | |
| | | var teamId:String = "" |
| | | var answerId:Int = 0 |
| | | if self.viewModel.selectIndex.value?.section == 1{ |
| | | teamId = self.listenNewModel.data?.id.components(separatedBy: ",")[self.page + 1] ?? "" |
| | | answerId = answerModel!.id |
| | | }else{ |
| | | teamId = self.listenNewModel.data?.id.components(separatedBy: ",")[self.page] ?? "" |
| | | answerId = answerModel!.id |
| | | } |
| | | |
| | | self.rootViewModel.insertCorrectAnswer(teamId: teamId, answerId: answerId) |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | self.voicePlayer.playerAt(url: tempSubV!.voiceUrl) |
| | | } |
| | | |
| | | //防止重复答题造成计数错误的问题 |
| | | if !answerIndexs.contains(answerIndex!){ |
| | | rootViewModel.correctNum += 1 |
| | | let v = rootViewModel.answerCount.value + 1 |
| | | rootViewModel.answerCount.accept(v) |
| | | } |
| | | |
| | | //正确才记录回答 |
| | | answerIndexs.insert(answerIndex!) |
| | | |
| | | }else{ |
| | | answerType = .fail |
| | | voicePlayer.playFailVoice() |
| | | rootViewModel.errorNum += 1 |
| | | } |
| | | voicePlayer.playerEnd() |
| | | |
| | | switch answerType { |
| | | case .success: |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+0.4) { |
| | | if let copyView = tempSubV?.copyView(){ |
| | | tempSubV?.alpha = 0 |
| | | let newRect = tempSubV!.view_handle.convert(tempSubV!.bounds, to: self.view) |
| | | copyView.frame = CGRect(origin: newRect.origin, size: CGSize(width: 159, height: 52)) |
| | | copyView.view_state.isHidden = true |
| | | copyView.isCopy = true |
| | | copyView.isPlaying() |
| | | copyView.btn_isAnswer.setImage(self.viewModel.selectIndex.value?.row == 1 ? UIImage(named: "icon_question"):UIImage(named: "icon_answer"), for: .normal) |
| | | copyView.img_play.alpha = 1 |
| | | copyView.voiceUrl = tempSubV!.voiceUrl |
| | | self.view.addSubview(copyView) |
| | | self.view.layoutIfNeeded() |
| | | |
| | | //获取Cell的顶部试图 |
| | | if let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_4_CCell", for: self.answerIndex!) as? ListenFight_lesson_4_CCell{ |
| | | var newRect1 = cell.convert(cell.bounds, to: self.collectionView) |
| | | newRect1.origin.x += 76 |
| | | newRect1.origin.y += 33 |
| | | |
| | | self.collectionView.reloadData() |
| | | UIView.animate(withDuration: 0.4) { |
| | | copyView.frame = CGRect(origin: newRect1.origin, size: CGSize(width: 159, height: 52)) |
| | | } completion: { _ in |
| | | self.viewModel.selectIndex.accept(nil) |
| | | for v in self.stackView.subviews{ |
| | | v.removeFromSuperview() |
| | | } |
| | | self.stackView.layoutIfNeeded() |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | case .fail: |
| | | self.view.isUserInteractionEnabled = false |
| | | UIView.animate(withDuration: 0.4) { |
| | | tempSubV?.img_state.alpha = 1 |
| | | }completion: { _ in |
| | | |
| | | UIView.animate(withDuration: 0.4, delay: 2.0) { |
| | | tempSubV?.img_state.alpha = 0 |
| | | } |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | self.setAnswerStackView() |
| | | self.view.isUserInteractionEnabled = true |
| | | } |
| | | } |
| | | case .none: |
| | | break |
| | | } |
| | | } |
| | | |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | super.viewDidAppear(animated) |
| | | voicePlayer.delegate = self |
| | | } |
| | | |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | voicePlayer.delegate = nil |
| | | VoicePlayer.share().playerInterrupt() |
| | | } |
| | | |
| | | |
| | | override func setUI() { |
| | | super.setUI() |
| | | |
| | | if !view.subviews.contains(collectionView){ |
| | | collectionView.delegate = self |
| | | collectionView.dataSource = self |
| | | collectionView.showsVerticalScrollIndicator = false |
| | | collectionView.backgroundColor = .clear |
| | | view.addSubview(collectionView) |
| | | } |
| | | |
| | | collectionView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(0) |
| | | make.left.equalTo(164) |
| | | make.width.equalTo(JQ_ScreenW - 164 * 2) |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | } |
| | | |
| | | override func setRx() { |
| | | NotificationCenter.default.rx.notification(ResetLession_Noti).take(until: self.rx.deallocated).subscribe(onNext: {[weak self]_ in |
| | | self?.restore() |
| | | self?.viewDidLoad() |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | | let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | flowLayout.minimumLineSpacing = 20 |
| | | flowLayout.minimumInteritemSpacing = 20 |
| | | |
| | | let w = (collectionView.size.width - flowLayout.minimumLineSpacing) / 2 |
| | | let h = (collectionView.size.height - flowLayout.minimumInteritemSpacing) / 2.1 |
| | | |
| | | if flowLayout.itemSize.width != w || flowLayout.itemSize.height != h{ |
| | | flowLayout.itemSize = CGSize(width: w, height: h - 20) |
| | | collectionView.reloadData() |
| | | } |
| | | } |
| | | |
| | | private func setAnswerStackView(){ |
| | | |
| | | guard let selectIndex = viewModel.selectIndex.value else{ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | |
| | | for v in stackView.subviews{ |
| | | v.removeFromSuperview() |
| | | } |
| | | |
| | | if !view.subviews.contains(stackView){ |
| | | view.addSubview(stackView) |
| | | stackView.snp.makeConstraints { make in |
| | | make.right.equalToSuperview().offset(-14) |
| | | make.centerY.equalToSuperview() |
| | | } |
| | | } |
| | | |
| | | let answerModel = filterItems[selectIndex.section].filter({$0.isQuestion == 0}).first! |
| | | let answerId = answerModel.id |
| | | |
| | | var tempVoiceArray = [String]() |
| | | for v in filterItems[selectIndex.section]{ |
| | | if v.id == answerId{ |
| | | tempVoiceArray.append(v.correct) |
| | | tempVoiceArray.append(v.error.components(separatedBy: ",").first ?? "") |
| | | tempVoiceArray.append(v.error.components(separatedBy: ",").last ?? "") |
| | | } |
| | | } |
| | | |
| | | tempVoiceArray.shuffle() |
| | | |
| | | |
| | | for i in 0...2{ |
| | | let answerView = Lesson_4_AnswerView.jq_loadNibView() |
| | | answerView.btn_choose.tag = 10 + i |
| | | answerView.tag = 20 + i |
| | | answerView.voiceUrl = tempVoiceArray[i] |
| | | answerView.btn_isAnswer.setImage(viewModel.selectIndex.value?.row == 1 ? UIImage(named: "icon_question"):UIImage(named: "icon_answer"), for: .normal) |
| | | answerView.btn_choose.addTarget(self, action: #selector(answerAction), for: .touchUpInside) |
| | | answerView.alpha = 0 |
| | | answerView.snp.makeConstraints { make in |
| | | make.width.equalTo(221) |
| | | make.height.equalTo(52) |
| | | } |
| | | answerView.btn_choose.isEnabled = false |
| | | answerView.playAt { index in |
| | | answerView.btn_choose.isEnabled = true |
| | | } |
| | | |
| | | // UIView.animate(withDuration: 0.05 + Double(i)) { |
| | | answerView.alpha = 1 |
| | | // } |
| | | |
| | | stackView.insertArrangedSubview(answerView, at: 0) |
| | | } |
| | | } |
| | | |
| | | func handleClouseAction(clouse:@escaping ()->Void){ |
| | | self.handleClouse = clouse |
| | | } |
| | | |
| | | @objc func answerAction(btn:UIButton){ |
| | | |
| | | var islistenDone:Int = 0 |
| | | for v in stackView.arrangedSubviews as! [Lesson_4_AnswerView]{ |
| | | if v.btn_choose.isEnabled == true{ |
| | | islistenDone += 1 |
| | | } |
| | | } |
| | | |
| | | if islistenDone != 3{ |
| | | alertError(msg: "请先听完");return |
| | | } |
| | | |
| | | guard let selectIndex = viewModel.selectIndex.value else{ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | |
| | | var tempSubV:Lesson_4_AnswerView? |
| | | |
| | | for subV in stackView.arrangedSubviews{ |
| | | if let s = subV as? Lesson_4_AnswerView{ |
| | | s.btn_choose.isSelected = btn.tag == s.btn_choose.tag |
| | | if s.btn_choose.isSelected{tempSubV = s} |
| | | } |
| | | } |
| | | |
| | | var answerModel:Listen1SubModel? |
| | | for (index,v) in filterItems[selectIndex.section].enumerated(){ |
| | | let m = filterItems[selectIndex.section].filter({$0.isQuestion == 0}).first |
| | | if v.id == m?.id{ |
| | | answerModel = v |
| | | answerIndex = IndexPath(row: index, section: viewModel.selectIndex.value!.section) |
| | | } |
| | | } |
| | | |
| | | var answerType:Fight_lessonType = .none |
| | | if tempSubV?.voiceUrl == answerModel?.correct{ |
| | | answerType = .success |
| | | voicePlayer.playSuccessVoice() |
| | | |
| | | |
| | | let model = listenNewModel.list[self.page] |
| | | model.status = 2 |
| | | Services.answerQuestion(id: model.id, status: 2).subscribe(onNext: {_ in |
| | | self.handleClouse?() |
| | | }).disposed(by: self.disposeBag) |
| | | |
| | | |
| | | var teamId:String = "" |
| | | var answerId:Int = 0 |
| | | if self.viewModel.selectIndex.value?.section == 1{ |
| | | teamId = self.listenNewModel.data?.id.components(separatedBy: ",")[self.page + 1] ?? "" |
| | | answerId = answerModel!.id |
| | | }else{ |
| | | teamId = self.listenNewModel.data?.id.components(separatedBy: ",")[self.page] ?? "" |
| | | answerId = answerModel!.id |
| | | } |
| | | |
| | | self.rootViewModel.insertCorrectAnswer(teamId: teamId, answerId: answerId) |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | self.voicePlayer.playerAt(url: tempSubV!.voiceUrl) |
| | | } |
| | | |
| | | //防止重复答题造成计数错误的问题 |
| | | if !answerIndexs.contains(answerIndex!){ |
| | | rootViewModel.correctNum += 1 |
| | | let v = rootViewModel.answerCount.value + 1 |
| | | rootViewModel.answerCount.accept(v) |
| | | } |
| | | |
| | | //正确才记录回答 |
| | | answerIndexs.insert(answerIndex!) |
| | | |
| | | }else{ |
| | | answerType = .fail |
| | | voicePlayer.playFailVoice() |
| | | rootViewModel.errorNum += 1 |
| | | |
| | | let model = listenNewModel.list[self.page] |
| | | model.status = 3 |
| | | Services.answerQuestion(id: model.id, status: 3).subscribe(onNext: {_ in |
| | | self.handleClouse?() |
| | | }).disposed(by: self.disposeBag) |
| | | } |
| | | voicePlayer.playerEnd() |
| | | |
| | | switch answerType { |
| | | case .success: |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+0.4) { |
| | | if let copyView = tempSubV?.copyView(){ |
| | | tempSubV?.alpha = 0 |
| | | let newRect = tempSubV!.view_handle.convert(tempSubV!.bounds, to: self.view) |
| | | copyView.frame = CGRect(origin: newRect.origin, size: CGSize(width: 159, height: 40)) |
| | | copyView.view_state.isHidden = true |
| | | copyView.isCopy = true |
| | | copyView.isPlaying() |
| | | copyView.btn_isAnswer.setImage(self.viewModel.selectIndex.value?.row == 1 ? UIImage(named: "icon_question"):UIImage(named: "icon_answer"), for: .normal) |
| | | copyView.img_play.alpha = 1 |
| | | copyView.voiceUrl = tempSubV!.voiceUrl |
| | | self.view.addSubview(copyView) |
| | | self.view.layoutIfNeeded() |
| | | |
| | | //获取Cell的顶部试图 |
| | | if let cell = self.collectionView.cellForItem(at: self.answerIndex!) as? ListenFight_lesson_4_CCell{ |
| | | var newRect1 = cell.convert(cell.bounds, to: self.collectionView) |
| | | newRect1.origin.x += 76 |
| | | newRect1.origin.y += 33 |
| | | |
| | | self.collectionView.reloadData() |
| | | UIView.animate(withDuration: 0.4) { |
| | | copyView.frame = CGRect(origin: newRect1.origin, size: CGSize(width: newRect1.width, height: 40)) |
| | | } completion: { _ in |
| | | self.viewModel.selectIndex.accept(nil) |
| | | for v in self.stackView.subviews{ |
| | | v.removeFromSuperview() |
| | | } |
| | | self.stackView.layoutIfNeeded() |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | case .fail: |
| | | self.view.isUserInteractionEnabled = false |
| | | UIView.animate(withDuration: 0.4) { |
| | | tempSubV?.img_state.alpha = 1 |
| | | }completion: { _ in |
| | | |
| | | UIView.animate(withDuration: 0.4, delay: 2.0) { |
| | | tempSubV?.img_state.alpha = 0 |
| | | } |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | self.setAnswerStackView() |
| | | self.view.isUserInteractionEnabled = true |
| | | } |
| | | } |
| | | case .none: |
| | | break |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_4_VC:UICollectionViewDelegate{ |
| | | |
| | | |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_4_VC:UICollectionViewDelegateFlowLayout{ |
| | | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { |
| | | return CGSize.zero |
| | | } |
| | | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { |
| | | return CGSizeMake(JQ_ScreenW, 20) |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_4_VC:UICollectionViewDataSource{ |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_4_CCell", for: indexPath) as! ListenFight_lesson_4_CCell |
| | | cell.backgroundColor = .clear |
| | | cell.contentView.backgroundColor = .clear |
| | | cell.indexPath = indexPath |
| | | cell.playAtIndex {[weak self] index in |
| | | guard let weakSelf = self else { return } |
| | | weakSelf.viewModel.selectIndex.accept(index) |
| | | UIView.animate(withDuration: 0.5) { |
| | | weakSelf.collectionView.snp.remakeConstraints { make in |
| | | make.left.equalTo(76) |
| | | make.top.equalTo(weakSelf.view.safeAreaLayoutGuide.snp.top).offset(0) |
| | | make.width.equalTo(JQ_ScreenW - 164 * 2) |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | weakSelf.view.layoutIfNeeded() |
| | | }completion: { _ in |
| | | weakSelf.setAnswerStackView() |
| | | } |
| | | } |
| | | let m = filterItems[indexPath.section][indexPath.row] |
| | | if indexPath == answerIndex{ |
| | | UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.4,options: .layoutSubviews) { |
| | | cell.img_state.alpha = 1 |
| | | cell.img_state.transform = .init(scaleX: 1.0, y: 1.0) |
| | | } |
| | | UIView.animate(withDuration: 0.4, delay: 3.0) { |
| | | cell.img_state.alpha = 0 |
| | | cell.img_state.transform = .init(scaleX: 0.1, y: 0.1) |
| | | } |
| | | }else{ |
| | | cell.img_state.alpha = 0 |
| | | cell.img_state.transform = .init(scaleX: 0.1, y: 0.1) |
| | | } |
| | | |
| | | if indexPath.row == 1{ |
| | | cell.btn_handle.setImage(UIImage(named: "icon_answer"), for: .normal) |
| | | cell.view_handle.isHidden = true |
| | | }else{ |
| | | cell.btn_handle.setImage(UIImage(named: "icon_question"), for: .normal) |
| | | cell.view_handle.isHidden = true |
| | | } |
| | | |
| | | //问题 |
| | | if filterItems[indexPath.section][indexPath.row].isQuestion == 0{ |
| | | cell.view_handle.isHidden = true |
| | | }else{ |
| | | cell.view_handle.isHidden = false |
| | | } |
| | | cell.setModel(m) |
| | | |
| | | |
| | | if answerIndexs.count == 0 && indexPath.section == 1{ |
| | | cell.view_handle.backgroundColor = .gray.withAlphaComponent(0.5) |
| | | cell.view_handle.isEnabled = false |
| | | }else{ |
| | | cell.view_handle.backgroundColor = UIColor(hexString: "#41A2EB") |
| | | cell.view_handle.isEnabled = true |
| | | } |
| | | |
| | | return cell |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { |
| | | let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) |
| | | return reusableView |
| | | |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | return filterItems[section].count |
| | | } |
| | | |
| | | func numberOfSections(in collectionView: UICollectionView) -> Int { |
| | | return filterItems.count |
| | | } |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_4_CCell", for: indexPath) as! ListenFight_lesson_4_CCell |
| | | cell.backgroundColor = .white |
| | | cell.contentView.backgroundColor = .white |
| | | cell.indexPath = indexPath |
| | | cell.playAtIndex {[weak self] index in |
| | | guard let weakSelf = self else { return } |
| | | weakSelf.viewModel.selectIndex.accept(index) |
| | | UIView.animate(withDuration: 0.5) { |
| | | weakSelf.collectionView.snp.remakeConstraints { make in |
| | | make.left.equalTo(76) |
| | | make.top.equalTo(weakSelf.view.safeAreaLayoutGuide.snp.top).offset(0) |
| | | make.width.equalTo(JQ_ScreenW - 164 * 2) |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | weakSelf.view.layoutIfNeeded() |
| | | }completion: { _ in |
| | | weakSelf.setAnswerStackView() |
| | | } |
| | | } |
| | | let m = filterItems[indexPath.section][indexPath.row] |
| | | if indexPath == answerIndex{ |
| | | UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.4,options: .layoutSubviews) { |
| | | cell.img_state.alpha = 1 |
| | | cell.img_state.transform = .init(scaleX: 1.0, y: 1.0) |
| | | } |
| | | UIView.animate(withDuration: 0.4, delay: 3.0) { |
| | | cell.img_state.alpha = 0 |
| | | cell.img_state.transform = .init(scaleX: 0.1, y: 0.1) |
| | | } |
| | | }else{ |
| | | cell.img_state.alpha = 0 |
| | | cell.img_state.transform = .init(scaleX: 0.1, y: 0.1) |
| | | } |
| | | |
| | | if indexPath.row == 1{ |
| | | cell.btn_handle.setImage(UIImage(named: "icon_answer"), for: .normal) |
| | | cell.view_handle.isHidden = true |
| | | }else{ |
| | | cell.btn_handle.setImage(UIImage(named: "icon_question"), for: .normal) |
| | | cell.view_handle.isHidden = true |
| | | } |
| | | |
| | | //问题 |
| | | if filterItems[indexPath.section][indexPath.row].isQuestion == 0{ |
| | | cell.view_handle.isHidden = true |
| | | }else{ |
| | | cell.view_handle.isHidden = false |
| | | } |
| | | cell.setModel(m) |
| | | |
| | | |
| | | if answerIndexs.count == 0 && indexPath.section == 1{ |
| | | cell.view_handle.backgroundColor = UIColor(hexString: "#D4D2CD") |
| | | cell.view_handle.isEnabled = false |
| | | }else{ |
| | | cell.view_handle.backgroundColor = .white |
| | | cell.view_handle.isEnabled = true |
| | | } |
| | | |
| | | return cell |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { |
| | | let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) |
| | | return reusableView |
| | | |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | return filterItems[section].count |
| | | } |
| | | |
| | | func numberOfSections(in collectionView: UICollectionView) -> Int { |
| | | return filterItems.count |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_4_VC:VoicePlayerDelegate{ |
| | | func playComplete() { |
| | | self.view.isUserInteractionEnabled = true |
| | | |
| | | |
| | | for subV in view.subviews { |
| | | if let s = subV as? Lesson_4_AnswerView{ |
| | | s.playEnd() |
| | | s.img_play.isHidden = false |
| | | s.img_play.alpha = 1 |
| | | } |
| | | } |
| | | |
| | | |
| | | for subV in stackView.arrangedSubviews{ |
| | | if let s = subV as? Lesson_4_AnswerView{ |
| | | s.playEnd() |
| | | } |
| | | } |
| | | |
| | | if let indexPath = viewModel.selectIndex.value ,let cell = collectionView.cellForItem(at: indexPath) as? ListenFight_lesson_4_CCell{ |
| | | cell.playEnd() |
| | | } |
| | | |
| | | //回答完成,下一答题 |
| | | if answerIndexs.count == 2{ |
| | | voicePlayer.playerEnd() |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: nil) |
| | | return |
| | | } |
| | | } |
| | | |
| | | func playing() { |
| | | self.view.isUserInteractionEnabled = false |
| | | } |
| | | func playComplete() { |
| | | self.view.isUserInteractionEnabled = true |
| | | |
| | | for subV in view.subviews { |
| | | if let s = subV as? Lesson_4_AnswerView{ |
| | | s.playEnd() |
| | | s.img_play.isHidden = false |
| | | s.img_play.alpha = 1 |
| | | } |
| | | } |
| | | |
| | | |
| | | let temp = stackView.subviews as! [Lesson_4_AnswerView] |
| | | for subV in temp.reversed() { |
| | | if subV.isplayend == false{ |
| | | voicePlayer.playerAt(url: subV.voiceUrl ?? "") |
| | | subV.isPlaying() |
| | | subV.btn_choose.isEnabled = true |
| | | break |
| | | }else{ |
| | | subV.playEnd() |
| | | } |
| | | } |
| | | |
| | | if let indexPath = viewModel.selectIndex.value ,let cell = collectionView.cellForItem(at: indexPath) as? ListenFight_lesson_4_CCell{ |
| | | cell.playEnd() |
| | | } |
| | | |
| | | //回答完成,下一答题 |
| | | if answerIndexs.count == 2{ |
| | | voicePlayer.playerEnd() |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: nil) |
| | | return |
| | | } |
| | | } |
| | | |
| | | func playing() { |
| | | self.view.isUserInteractionEnabled = false |
| | | } |
| | | } |
| | |
| | | |
| | | class HomeListenFight_lesson_5_VC: BaseVC { |
| | | |
| | | private var viewModel = FightAnswerViewModel() |
| | | private var listenNewModel:ListenNewModel! |
| | | private var page:Int! |
| | | private var answterCount:Int = 0 //回答计数,用于确定角标 |
| | | private var playVoiceAt:Int? //播放声音的View |
| | | private var playVoiceRealAt:Int? //播放声音的View -被乱序后,真实Index |
| | | var rootViewModel:HomeListenFightViewModel! |
| | | var teamScheduleModel:TeamScheduleModel? |
| | | private var voicePlayer = VoicePlayer.share() |
| | | private var isListen:Bool = false |
| | | private var viewModel = FightAnswerViewModel() |
| | | private var listenNewModel:ListenNewModel! |
| | | private var page:Int! |
| | | private var answterCount:Int = 0 //回答计数,用于确定角标 |
| | | private var playVoiceAt:Int? //播放声音的View |
| | | private var playVoiceRealAt:Int? //播放声音的View -被乱序后,真实Index |
| | | var rootViewModel:HomeListenFightViewModel! |
| | | var teamScheduleModel:TeamScheduleModel? |
| | | private var voicePlayer = VoicePlayer.share() |
| | | private var isListen:Bool = false |
| | | private var playingTags = Set<Int>() //已经播放过的TAG |
| | | private var handleClouse:(()->Void)? |
| | | |
| | | private lazy var collectionView:UICollectionView = { |
| | | let flowLayout = UICollectionViewFlowLayout() |
| | | let w = (JQ_ScreenW - 189 * 2 - 18) / 2.0 |
| | | flowLayout.itemSize = CGSize(width: w, height: w * 0.70) |
| | | flowLayout.minimumInteritemSpacing = 15 |
| | | flowLayout.minimumLineSpacing = 15 |
| | | flowLayout.scrollDirection = .vertical |
| | | let collection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) |
| | | collection.register(UINib(nibName: "ListenFight_lesson_1_CCell", bundle: nil), forCellWithReuseIdentifier: "_ListenFight_lesson_1_CCell") |
| | | collection.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header") |
| | | collection.isScrollEnabled = false |
| | | return collection |
| | | }() |
| | | private lazy var collectionView:UICollectionView = { |
| | | let flowLayout = UICollectionViewFlowLayout() |
| | | let w = (JQ_ScreenW - 189 * 2 - 18) / 2.0 |
| | | flowLayout.itemSize = CGSize(width: w, height: w * 0.70) |
| | | flowLayout.minimumInteritemSpacing = 15 |
| | | flowLayout.minimumLineSpacing = 15 |
| | | flowLayout.scrollDirection = .vertical |
| | | let collection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) |
| | | collection.register(UINib(nibName: "ListenFight_lesson_1_CCell", bundle: nil), forCellWithReuseIdentifier: "_ListenFight_lesson_1_CCell") |
| | | collection.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header") |
| | | collection.isScrollEnabled = false |
| | | return collection |
| | | }() |
| | | |
| | | private lazy var stackView:UIStackView = { |
| | | let sta = UIStackView() |
| | | sta.spacing = 89 |
| | | sta.distribution = .equalSpacing |
| | | sta.axis = .horizontal |
| | | return sta |
| | | }() |
| | | |
| | | private lazy var label_hint:UILabel = { |
| | | let label = UILabel() |
| | | label.font = .systemFont(ofSize: 18, weight: .medium) |
| | | label.textColor = .black |
| | | label.text = "语音对应内容" |
| | | label.textAlignment = .center |
| | | return label |
| | | }() |
| | | |
| | | required init(page:Int,listenNewModel:ListenNewModel){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.page = page |
| | | self.listenNewModel = listenNewModel |
| | | // self.listen1Model.subjectList.shuffle() |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | private lazy var stackView:UIStackView = { |
| | | let sta = UIStackView() |
| | | sta.spacing = 49 |
| | | sta.distribution = .equalSpacing |
| | | sta.axis = .horizontal |
| | | return sta |
| | | }() |
| | | |
| | | |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | required init(page:Int,listenNewModel:ListenNewModel){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.page = page |
| | | self.listenNewModel = listenNewModel |
| | | // self.listen1Model.subjectList.shuffle() |
| | | } |
| | | |
| | | collectionView.reloadData() |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | |
| | | |
| | | //回传记录,始终保持答题进度 |
| | | if let team = teamScheduleModel{ |
| | | for teamId in team.teamIds{ |
| | | for v in listenNewModel.subjectList[page]{ |
| | | if team.topicIds.contains(v.id){ |
| | | rootViewModel.insertCorrectAnswer(teamId: "\(teamId)", answerId: v.id) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | |
| | | collectionView.reloadData() |
| | | |
| | | |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | super.viewDidAppear(animated) |
| | | voicePlayer.delegate = self |
| | | } |
| | | //回传记录,始终保持答题进度 |
| | | if let team = teamScheduleModel{ |
| | | for teamId in team.teamIds{ |
| | | for v in listenNewModel.subjectList[page]{ |
| | | if team.topicIds.contains(v.id){ |
| | | rootViewModel.insertCorrectAnswer(teamId: "\(teamId)", answerId: v.id) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | voicePlayer.delegate = nil |
| | | VoicePlayer.share().playerInterrupt() |
| | | } |
| | | |
| | | override func setUI() { |
| | | super.setUI() |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | super.viewDidAppear(animated) |
| | | voicePlayer.delegate = self |
| | | } |
| | | |
| | | view.addSubview(stackView) |
| | | stackView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(19) |
| | | make.centerX.equalToSuperview() |
| | | make.height.equalTo(52) |
| | | } |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | voicePlayer.delegate = nil |
| | | VoicePlayer.share().playerInterrupt() |
| | | } |
| | | |
| | | view.addSubview(label_hint) |
| | | label_hint.snp.makeConstraints { make in |
| | | make.left.right.equalToSuperview() |
| | | make.top.equalTo(stackView.snp.bottom).offset(6) |
| | | make.height.equalTo(0) |
| | | } |
| | | override func setUI() { |
| | | super.setUI() |
| | | |
| | | collectionView.delegate = self |
| | | collectionView.dataSource = self |
| | | collectionView.showsVerticalScrollIndicator = false |
| | | collectionView.backgroundColor = .clear |
| | | view.addSubview(collectionView) |
| | | collectionView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.label_hint.snp.bottom).offset(11) |
| | | make.left.equalTo(189) |
| | | make.width.equalTo(JQ_ScreenW - 189 * 2) |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | view.addSubview(stackView) |
| | | stackView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(19) |
| | | make.centerX.equalToSuperview() |
| | | make.height.equalTo(52) |
| | | } |
| | | |
| | | showHintText(false) |
| | | collectionView.delegate = self |
| | | collectionView.dataSource = self |
| | | collectionView.showsVerticalScrollIndicator = false |
| | | collectionView.backgroundColor = .clear |
| | | view.addSubview(collectionView) |
| | | collectionView.snp.makeConstraints { make in |
| | | make.top.equalToSuperview().offset(91) |
| | | make.left.equalTo(189) |
| | | make.width.equalTo(JQ_ScreenW - 189 * 2) |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | |
| | | setAnswerStackView() |
| | | } |
| | | setAnswerStackView() |
| | | } |
| | | |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | | let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | | let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | |
| | | let w = (collectionView.size.width - flowLayout.minimumLineSpacing) / 2 |
| | | let h = (collectionView.size.height - flowLayout.minimumInteritemSpacing) / 2.0 |
| | | |
| | | if flowLayout.itemSize.width != w || flowLayout.itemSize.height != h{ |
| | | flowLayout.itemSize = CGSize(width: w, height: h) |
| | | collectionView.reloadData() |
| | | } |
| | | } |
| | | let w = (collectionView.size.width - flowLayout.minimumLineSpacing) / 2 |
| | | let h = (collectionView.size.height - flowLayout.minimumInteritemSpacing) / 2.0 |
| | | |
| | | override func setRx() { |
| | | if flowLayout.itemSize.width != w || flowLayout.itemSize.height != h{ |
| | | flowLayout.itemSize = CGSize(width: w, height: h) |
| | | collectionView.reloadData() |
| | | } |
| | | } |
| | | |
| | | } |
| | | override func setRx() { |
| | | NotificationCenter.default.rx.notification(ResetLession_Noti).take(until: self.rx.deallocated).subscribe(onNext: {[weak self]_ in |
| | | self?.restore() |
| | | self?.viewDidLoad() |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | func restore(){ |
| | | viewModel.answerType.accept(.none) |
| | | answterCount = 0 |
| | | playVoiceAt = nil |
| | | playVoiceRealAt = nil |
| | | func restore(){ |
| | | viewModel.answerType.accept(.none) |
| | | answterCount = 0 |
| | | playVoiceAt = nil |
| | | playVoiceRealAt = nil |
| | | |
| | | for subV in view.subviews{ |
| | | if subV is VoiceHandleView{ |
| | | subV.removeFromSuperview() |
| | | } |
| | | } |
| | | setUI() |
| | | collectionView.reloadData() |
| | | } |
| | | for subV in view.subviews{ |
| | | if subV is VoiceHandleView{ |
| | | subV.removeFromSuperview() |
| | | } |
| | | } |
| | | setUI() |
| | | collectionView.reloadData() |
| | | } |
| | | |
| | | private func setAnswerStackView(){ |
| | | private func setAnswerStackView(){ |
| | | |
| | | for subView in stackView.arrangedSubviews{ |
| | | subView.removeFromSuperview() |
| | | } |
| | | for subView in stackView.arrangedSubviews{ |
| | | subView.removeFromSuperview() |
| | | } |
| | | |
| | | if !view.subviews.contains(stackView){ |
| | | view.addSubview(stackView) |
| | | stackView.snp.makeConstraints { make in |
| | | make.right.equalToSuperview().offset(-14) |
| | | make.centerY.equalToSuperview() |
| | | } |
| | | } |
| | | if !view.subviews.contains(stackView){ |
| | | view.addSubview(stackView) |
| | | stackView.snp.makeConstraints { make in |
| | | make.right.equalToSuperview().offset(-14) |
| | | make.centerY.equalToSuperview() |
| | | } |
| | | } |
| | | |
| | | var tempArray = [VoiceHandleView]() |
| | | for (i,value) in listenNewModel.subjectList[page].enumerated(){ |
| | | let answerView = VoiceHandleView() |
| | | answerView.listenType = .lesson5 |
| | | answerView.tag = 1000+i |
| | | answerView.playAt {[weak self] index in |
| | | print("--->\(index)--\(answerView.frame.origin.x / 248)") |
| | | self?.playVoiceRealAt = Int(answerView.frame.origin.x) / 248 |
| | | self?.playVoiceAt = index - 1000 |
| | | self?.showHintText(true) |
| | | } |
| | | answerView.playUrl = value.correct |
| | | answerView.snp.makeConstraints { make in |
| | | make.width.equalTo(159) |
| | | make.height.equalTo(52) |
| | | } |
| | | var tempArray = [VoiceHandleView]() |
| | | for (i,value) in listenNewModel.subjectList[page].enumerated(){ |
| | | let answerView = VoiceHandleView() |
| | | answerView.listenType = .lesson5 |
| | | answerView.tag = 1000+i |
| | | answerView.playAt {[weak self] index in |
| | | self?.playVoiceRealAt = Int(answerView.frame.origin.x) / 248 |
| | | self?.playVoiceAt = index - 1000 |
| | | } |
| | | answerView.playUrl = value.correct |
| | | answerView.snp.makeConstraints { make in |
| | | make.width.equalTo(221) |
| | | make.height.equalTo(40) |
| | | } |
| | | |
| | | UIView.animate(withDuration: 0.05 + Double(i)) { |
| | | answerView.alpha = 1 |
| | | } |
| | | tempArray.append(answerView) |
| | | } |
| | | tempArray.shuffle() |
| | | stackView.addArrangedSubviews(tempArray) |
| | | } |
| | | UIView.animate(withDuration: 0.05 + Double(i)) { |
| | | answerView.alpha = 1 |
| | | } |
| | | tempArray.append(answerView) |
| | | } |
| | | tempArray.shuffle() |
| | | |
| | | private func showHintText(_ state:Bool){ |
| | | if let at = playVoiceAt{ |
| | | label_hint.text = listenNewModel.subjectList[page][at].name |
| | | UIView.animate(withDuration: 0.5) { |
| | | if state{ |
| | | self.label_hint.snp.remakeConstraints { make in |
| | | make.left.right.equalToSuperview() |
| | | make.top.equalTo(self.stackView.snp.bottom).offset(6) |
| | | make.height.equalTo(25) |
| | | } |
| | | }else{ |
| | | self.label_hint.snp.remakeConstraints { make in |
| | | make.left.right.equalToSuperview() |
| | | make.top.equalTo(self.stackView.snp.bottom).offset(6) |
| | | make.height.equalTo(0) |
| | | } |
| | | } |
| | | self.view.layoutIfNeeded() |
| | | } |
| | | } |
| | | } |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+1.0) { |
| | | //自动播放第一条 |
| | | tempArray.first?.playingAction() |
| | | self.playingTags.insert(tempArray.first?.tag ?? 0) |
| | | } |
| | | |
| | | stackView.addArrangedSubviews(tempArray) |
| | | } |
| | | |
| | | func handleClouseAction(clouse:@escaping ()->Void){ |
| | | self.handleClouse = clouse |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_5_VC:UICollectionViewDelegate{ |
| | | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
| | | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
| | | |
| | | if isListen == false{ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | isListen = false |
| | | if isListen == false{ |
| | | alertError(msg: "请先听题");return |
| | | } |
| | | isListen = false |
| | | |
| | | viewModel.selectIndex.accept(indexPath) |
| | | guard playVoiceAt != nil else {return} |
| | | viewModel.selectIndex.accept(indexPath) |
| | | guard playVoiceAt != nil else {return} |
| | | |
| | | let answer = listenNewModel.subjectList[page][playVoiceAt!] |
| | | let selectAnswer = listenNewModel.subjectList[page][indexPath.row] |
| | | let answer = listenNewModel.subjectList[page][playVoiceAt!] |
| | | let selectAnswer = listenNewModel.subjectList[page][indexPath.row] |
| | | |
| | | var answerType:Fight_lessonType = .none |
| | | if answer.id == selectAnswer.id{ |
| | | answerType = .success |
| | | voicePlayer.playSuccessVoice() |
| | | let teamId = listenNewModel.data?.id.components(separatedBy: ",")[page] |
| | | rootViewModel.insertCorrectAnswer(teamId: teamId, answerId: selectAnswer.id) |
| | | }else{ |
| | | answerType = .fail |
| | | voicePlayer.playFailVoice() |
| | | } |
| | | var answerType:Fight_lessonType = .none |
| | | if answer.id == selectAnswer.id{ |
| | | answerType = .success |
| | | voicePlayer.playSuccessVoice() |
| | | let teamId = listenNewModel.data?.id.components(separatedBy: ",")[page] |
| | | rootViewModel.insertCorrectAnswer(teamId: teamId, answerId: selectAnswer.id) |
| | | |
| | | let tempSubV = stackView.arrangedSubviews[self.playVoiceRealAt!] as! VoiceHandleView |
| | | label_hint.text = "" |
| | | |
| | | switch answerType { |
| | | case .success: |
| | | answterCount += 1 |
| | | rootViewModel.correctNum += 1 |
| | | viewModel.answerType.accept(.success) |
| | | let data = listenNewModel.list[page] |
| | | data.status = 2 |
| | | Services.answerQuestion(id: data.id, status: 2).subscribe(onNext: {[weak self]_ in |
| | | self?.handleClouse?() |
| | | }).disposed(by: disposeBag) |
| | | }else{ |
| | | answerType = .fail |
| | | voicePlayer.playFailVoice() |
| | | |
| | | let copyView = tempSubV.copyView() |
| | | copyView.listenType = .lesson5 |
| | | copyView.jq_cornerRadius = 0 |
| | | copyView.playUrl = selectAnswer.correct |
| | | let newRect = tempSubV.convert(tempSubV.bounds, to: self.view) |
| | | copyView.frame = CGRect(origin: newRect.origin, size: CGSize(width: 159, height: 52)) |
| | | self.view.addSubview(copyView) |
| | | tempSubV.alpha = 0 |
| | | let data = listenNewModel.list[page] |
| | | data.status = 3 |
| | | Services.answerQuestion(id: data.id, status: 3).subscribe(onNext: {[weak self]_ in |
| | | self?.handleClouse?() |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+0.4) { |
| | | let tempSubV = stackView.arrangedSubviews[self.playVoiceRealAt!] as! VoiceHandleView |
| | | |
| | | //获取Cell的顶部试图 |
| | | let flowLayout = self.collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | switch answerType { |
| | | case .success: |
| | | answterCount += 1 |
| | | rootViewModel.correctNum += 1 |
| | | viewModel.answerType.accept(.success) |
| | | |
| | | if let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_1_CCell", for: indexPath) as? ListenFight_lesson_1_CCell{ |
| | | var newRect1 = cell.convert(cell.bounds, to: self.collectionView) |
| | | newRect1.origin.x += (collectionView.frame.origin.x + 5) |
| | | newRect1.origin.y += 94 + 24 |
| | | let copyView = tempSubV.copyView() |
| | | copyView.isPlayed = true |
| | | copyView.listenType = .lesson5 |
| | | copyView.jq_cornerRadius = 8 |
| | | copyView.playUrl = selectAnswer.correct |
| | | let newRect = tempSubV.convert(tempSubV.bounds, to: self.view) |
| | | copyView.frame = CGRect(origin: newRect.origin, size: CGSize(width: 221, height: 52)) |
| | | self.view.addSubview(copyView) |
| | | tempSubV.alpha = 0 |
| | | |
| | | UIView.animateKeyframes(withDuration: 0.4, delay: 0,options: .calculationModeLinear) { |
| | | copyView.frame = CGRect(origin: newRect1.origin, size: CGSize(width: flowLayout.itemSize.width - 10 , height: 40)) |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+0.4) { |
| | | |
| | | }completion: { _ in |
| | | copyView.playingAction() |
| | | self.playVoiceRealAt = nil |
| | | self.playVoiceAt = nil |
| | | self.collectionView.reloadData() |
| | | } |
| | | } |
| | | } |
| | | //获取Cell的顶部试图 |
| | | let flowLayout = self.collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | if let cell = self.collectionView.cellForItem(at: indexPath) as? ListenFight_lesson_1_CCell{ |
| | | var newRect1 = cell.convert(cell.bounds, to: self.collectionView) |
| | | newRect1.origin.x += (collectionView.frame.origin.x) |
| | | newRect1.origin.y += 94 |
| | | |
| | | case .fail: |
| | | viewModel.answerType.accept(.fail) |
| | | rootViewModel.errorNum += 1 |
| | | collectionView.reloadData() |
| | | case .none: |
| | | break |
| | | } |
| | | } |
| | | UIView.animateKeyframes(withDuration: 0.4, delay: 0,options: .calculationModeLinear) { |
| | | copyView.frame = CGRect(origin: newRect1.origin, size: CGSize(width: flowLayout.itemSize.width , height: 40)) |
| | | |
| | | }completion: { _ in |
| | | // copyView.playingAction() |
| | | self.playVoiceRealAt = nil |
| | | self.playVoiceAt = nil |
| | | self.collectionView.reloadData() |
| | | |
| | | //播放下一个 |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+1) { |
| | | for v in self.stackView.subviews as! [VoiceHandleView]{ |
| | | if !self.playingTags.contains(v.tag){ |
| | | v.playingAction() |
| | | self.playingTags.insert(v.tag) |
| | | break |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |
| | | } |
| | | } |
| | | |
| | | case .fail: |
| | | viewModel.answerType.accept(.fail) |
| | | rootViewModel.errorNum += 1 |
| | | collectionView.reloadData() |
| | | case .none: |
| | | break |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_5_VC:UICollectionViewDelegateFlowLayout{ |
| | | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { |
| | | return CGSize.zero |
| | | } |
| | | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { |
| | | return CGSize.zero |
| | | } |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_5_VC:UICollectionViewDataSource{ |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let model = listenNewModel.subjectList[page][indexPath.row] |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_1_CCell", for: indexPath) as! ListenFight_lesson_1_CCell |
| | | cell.jq_addShadows(shadowColor: .black.withAlphaComponent(0.31), corner: 8, radius: 3, offset: CGSize(width: 0, height: 1), opacity: 1) |
| | | if viewModel.selectIndex.value == indexPath{ |
| | | cell.setState(state: viewModel.answerType.value) |
| | | }else{ |
| | | cell.setState(state: .none) |
| | | } |
| | | cell.setListen1SubModel(model) |
| | | return cell |
| | | } |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let model = listenNewModel.subjectList[page][indexPath.row] |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_lesson_1_CCell", for: indexPath) as! ListenFight_lesson_1_CCell |
| | | cell.jq_addShadows(shadowColor: .black.withAlphaComponent(0.31), corner: 8, radius: 3, offset: CGSize(width: 0, height: 1), opacity: 1) |
| | | if viewModel.selectIndex.value == indexPath{ |
| | | cell.setState(state: viewModel.answerType.value) |
| | | }else{ |
| | | cell.setState(state: .none) |
| | | } |
| | | cell.setListen1SubModel(model) |
| | | return cell |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { |
| | | let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) |
| | | return reusableView |
| | | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { |
| | | let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) |
| | | return reusableView |
| | | |
| | | } |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | return listenNewModel.subjectList[page].count |
| | | } |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | return listenNewModel.subjectList[page].count |
| | | } |
| | | |
| | | func numberOfSections(in collectionView: UICollectionView) -> Int { |
| | | return 1 |
| | | } |
| | | func numberOfSections(in collectionView: UICollectionView) -> Int { |
| | | return 1 |
| | | } |
| | | |
| | | } |
| | | |
| | | extension HomeListenFight_lesson_5_VC:VoicePlayerDelegate{ |
| | | func playComplete() { |
| | | view.isUserInteractionEnabled = true |
| | | isListen = true |
| | | for subV in stackView.arrangedSubviews as! [VoiceHandleView]{ |
| | | subV.resetView() |
| | | } |
| | | func playComplete() { |
| | | view.isUserInteractionEnabled = true |
| | | isListen = true |
| | | for subV in stackView.arrangedSubviews as! [VoiceHandleView]{ |
| | | subV.resetView() |
| | | } |
| | | |
| | | if viewModel.answerType.value == .success{ |
| | | let v = rootViewModel.answerCount.value + 1 |
| | | rootViewModel.answerCount.accept(v) |
| | | viewModel.answerType.accept(.none) |
| | | } |
| | | if viewModel.answerType.value == .success{ |
| | | let v = rootViewModel.answerCount.value + 1 |
| | | rootViewModel.answerCount.accept(v) |
| | | viewModel.answerType.accept(.none) |
| | | } |
| | | |
| | | if self.answterCount >= 4{ |
| | | // DispatchQueue.main.asyncAfter(delay: 3.0) { |
| | | self.voicePlayer.playerEnd() |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: nil) |
| | | // } |
| | | } |
| | | } |
| | | |
| | | func playing() { |
| | | view.isUserInteractionEnabled = false |
| | | } |
| | | if self.answterCount >= 4{ |
| | | // DispatchQueue.main.asyncAfter(delay: 3.0) { |
| | | self.voicePlayer.playerEnd() |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: nil) |
| | | // } |
| | | } |
| | | } |
| | | |
| | | func playing() { |
| | | view.isUserInteractionEnabled = false |
| | | } |
| | | |
| | | } |
| | |
| | | |
| | | class HomeListenGame_1_VC: BaseVC { |
| | | |
| | | private var viewModel = FightAnswerViewModel() |
| | | private var listen1Model:Listen1Model! |
| | | private var viewModel = FightAnswerViewModel() |
| | | private var listen1Model:Listen1Model! |
| | | |
| | | var rootViewModel:HomeListenFightViewModel! |
| | | var rootViewModel:HomeListenFightViewModel! |
| | | |
| | | private var totalCount:Int = 1 //游戏的总数量 |
| | | private var totalCount:Int = 1 //游戏的总数量 |
| | | |
| | | private lazy var label_class:UILabel = { |
| | | let label = UILabel() |
| | | label.textColor = .white |
| | | label.text = "1" |
| | | label.textAlignment = .center |
| | | label.font = UIFont.init(name: "Impact", size: 21) |
| | | return label |
| | | private lazy var label_class:UILabel = { |
| | | let label = UILabel() |
| | | label.textColor = .white |
| | | label.text = "1" |
| | | label.textAlignment = .center |
| | | label.font = UIFont.init(name: "Impact", size: 21) |
| | | return label |
| | | |
| | | }() |
| | | }() |
| | | |
| | | private let view_class_title = UIView() |
| | | private let view_class_title = UIView() |
| | | |
| | | private lazy var label_hint:UILabel = { |
| | | let label = UILabel() |
| | | label.textColor = UIColor(hexStr: "#EE1111") |
| | | label.text = "请在10s内选择答案!" |
| | | label.textAlignment = .center |
| | | label.isHidden = true |
| | | label.font = .systemFont(ofSize: 14, weight: .medium) |
| | | return label |
| | | }() |
| | | private lazy var label_hint:UILabel = { |
| | | let label = UILabel() |
| | | label.textColor = UIColor(hexStr: "#EE1111") |
| | | label.text = "请在10s内选择答案!" |
| | | label.textAlignment = .center |
| | | label.isHidden = true |
| | | label.font = .systemFont(ofSize: 14, weight: .medium) |
| | | return label |
| | | }() |
| | | |
| | | private lazy var view_studyHandleView:VoiceHandleView = { |
| | | let studyHandleView = VoiceHandleView() |
| | | return studyHandleView |
| | | }() |
| | | private lazy var view_studyHandleView:VoiceHandleView = { |
| | | let studyHandleView = VoiceHandleView() |
| | | return studyHandleView |
| | | }() |
| | | |
| | | private lazy var collectionView:UICollectionView = { |
| | | let flowLayout = UICollectionViewFlowLayout() |
| | | flowLayout.minimumInteritemSpacing = 3 |
| | | flowLayout.minimumLineSpacing = 3 |
| | | flowLayout.scrollDirection = .vertical |
| | | let collection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) |
| | | collection.contentInset = UIEdgeInsets(top: 0, left: 35, bottom: 0, right: 35) |
| | | collection.register(UINib(nibName: "ListenFight_Game_CCell", bundle: nil), forCellWithReuseIdentifier: "_ListenFight_Game_CCell") |
| | | collection.isScrollEnabled = false |
| | | return collection |
| | | }() |
| | | private lazy var collectionView:UICollectionView = { |
| | | let flowLayout = UICollectionViewFlowLayout() |
| | | flowLayout.minimumInteritemSpacing = 3 |
| | | flowLayout.minimumLineSpacing = 3 |
| | | flowLayout.scrollDirection = .vertical |
| | | let collection = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) |
| | | collection.contentInset = UIEdgeInsets(top: 0, left: 35, bottom: 0, right: 35) |
| | | collection.register(UINib(nibName: "ListenFight_Game_CCell", bundle: nil), forCellWithReuseIdentifier: "_ListenFight_Game_CCell") |
| | | collection.isScrollEnabled = false |
| | | return collection |
| | | }() |
| | | |
| | | private var timer:Timer? |
| | | private var times:Int = 10 |
| | | private var voicePlayer = VoicePlayer.share() |
| | | private var timer:Timer? |
| | | private var times:Int = 10 |
| | | private var voicePlayer = VoicePlayer.share() |
| | | |
| | | private var answerSet = Set<Listen1SubModel>() |
| | | private var currentAnswer:Listen1SubModel?{ |
| | | didSet{ |
| | | if let v = currentAnswer{ |
| | | view_studyHandleView.playUrl = v.correct |
| | | voicePlayer.playerAt(url: v.correct) |
| | | viewModel.answerType.accept(.none) |
| | | } |
| | | } |
| | | } |
| | | private var answerSet = Set<Listen1SubModel>() |
| | | private var currentAnswer:Listen1SubModel?{ |
| | | didSet{ |
| | | if let v = currentAnswer{ |
| | | view_studyHandleView.playUrl = v.correct |
| | | voicePlayer.playerAt(url: v.correct) |
| | | viewModel.answerType.accept(.none) |
| | | } |
| | | } |
| | | } |
| | | |
| | | required init(listen1Model:Listen1Model){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.listen1Model = listen1Model |
| | | } |
| | | required init(listen1Model:Listen1Model){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.listen1Model = listen1Model |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | super.viewDidAppear(animated) |
| | | voicePlayer.delegate = self |
| | | } |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | super.viewDidAppear(animated) |
| | | voicePlayer.delegate = self |
| | | } |
| | | |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | timer?.invalidate() |
| | | timer = nil |
| | | voicePlayer.delegate = nil |
| | | voicePlayer.playerInterrupt() |
| | | } |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | timer?.invalidate() |
| | | timer = nil |
| | | voicePlayer.delegate = nil |
| | | voicePlayer.playerInterrupt() |
| | | } |
| | | |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | |
| | | if listen1Model != nil{ |
| | | times = (listen1Model?.data?.time ?? 10) + 1 |
| | | collectionView.reloadData() |
| | | label_hint.isHidden = false |
| | | label_hint.text = "准备听题" |
| | | if listen1Model != nil{ |
| | | times = (listen1Model?.data?.time ?? 10) + 1 |
| | | collectionView.reloadData() |
| | | label_hint.isHidden = false |
| | | label_hint.text = "准备听题" |
| | | |
| | | for v in listen1Model?.subjectList ?? []{ |
| | | answerSet.insert(v) |
| | | } |
| | | for v in listen1Model?.subjectList ?? []{ |
| | | answerSet.insert(v) |
| | | } |
| | | |
| | | print("--->开始答题:剩余:\(answerSet.count)") |
| | | print("--->开始答题:剩余:\(answerSet.count)") |
| | | |
| | | if listen1Model.data?.playNow == true{ |
| | | self.currentAnswer = self.answerSet.randomElement() //随机 |
| | | if self.timer == nil{self.startTimer()} |
| | | }else{ |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | self.currentAnswer = self.answerSet.randomElement() //随机 |
| | | if self.timer == nil{self.startTimer()} |
| | | } |
| | | } |
| | | } |
| | | if listen1Model.data?.playNow == true{ |
| | | self.currentAnswer = self.answerSet.randomElement() //随机 |
| | | if self.timer == nil{self.startTimer()} |
| | | }else{ |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+2) { |
| | | self.currentAnswer = self.answerSet.randomElement() //随机 |
| | | if self.timer == nil{self.startTimer()} |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | override func setUI() { |
| | | super.setUI() |
| | | override func setUI() { |
| | | super.setUI() |
| | | |
| | | |
| | | view_class_title.jq_cornerRadius = 16 |
| | | view_class_title.backgroundColor = UIColor(hexStr: "#FBCF0F") |
| | | view.addSubview(view_class_title) |
| | | view_class_title.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(40) |
| | | make.left.equalToSuperview().offset(40) |
| | | make.height.equalTo(32) |
| | | make.width.greaterThanOrEqualTo(32) |
| | | } |
| | | view_class_title.jq_cornerRadius = 16 |
| | | view_class_title.backgroundColor = UIColor(hexStr: "#FBCF0F") |
| | | view.addSubview(view_class_title) |
| | | view_class_title.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(40) |
| | | make.left.equalToSuperview().offset(40) |
| | | make.height.equalTo(32) |
| | | make.width.greaterThanOrEqualTo(32) |
| | | } |
| | | |
| | | view_class_title.addSubview(label_class) |
| | | label_class.text = "\(totalCount)" |
| | | label_class.snp.makeConstraints { make in |
| | | make.left.equalTo(11) |
| | | make.right.equalTo(-12) |
| | | make.centerY.equalToSuperview() |
| | | } |
| | | view_class_title.addSubview(label_class) |
| | | label_class.text = "\(totalCount)" |
| | | label_class.snp.makeConstraints { make in |
| | | make.left.equalTo(11) |
| | | make.right.equalTo(-12) |
| | | make.centerY.equalToSuperview() |
| | | } |
| | | |
| | | |
| | | view.addSubview(view_studyHandleView) |
| | | view_studyHandleView.snp.makeConstraints { make in |
| | | make.left.equalTo(view_class_title.snp.right).offset(12) |
| | | make.centerY.equalTo(view_class_title) |
| | | make.width.equalTo(159) |
| | | make.height.equalTo(52) |
| | | } |
| | | view.addSubview(view_studyHandleView) |
| | | view_studyHandleView.snp.makeConstraints { make in |
| | | make.left.equalTo(view_class_title.snp.right).offset(12) |
| | | make.centerY.equalTo(view_class_title) |
| | | make.width.equalTo(159) |
| | | make.height.equalTo(52) |
| | | } |
| | | |
| | | view.addSubview(label_hint) |
| | | label_hint.snp.makeConstraints { make in |
| | | make.left.equalTo(view_studyHandleView.snp.right).offset(23) |
| | | make.centerY.equalTo(view_class_title) |
| | | make.height.equalTo(20) |
| | | } |
| | | view.addSubview(label_hint) |
| | | label_hint.snp.makeConstraints { make in |
| | | make.left.equalTo(view_studyHandleView.snp.right).offset(23) |
| | | make.centerY.equalTo(view_class_title) |
| | | make.height.equalTo(20) |
| | | } |
| | | |
| | | collectionView.delegate = self |
| | | collectionView.dataSource = self |
| | | collectionView.showsVerticalScrollIndicator = false |
| | | collectionView.jq_addShadows(shadowColor: UIColor.black.withAlphaComponent(0.1), corner: 8, radius: 10, offset: CGSize(width: 0, height: 2), opacity: 1) |
| | | collectionView.backgroundColor = .clear |
| | | view.addSubview(collectionView) |
| | | collectionView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(99) |
| | | make.left.right.equalToSuperview() |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | collectionView.delegate = self |
| | | collectionView.dataSource = self |
| | | collectionView.showsVerticalScrollIndicator = false |
| | | collectionView.jq_addShadows(shadowColor: UIColor.black.withAlphaComponent(0.1), corner: 8, radius: 10, offset: CGSize(width: 0, height: 2), opacity: 1) |
| | | collectionView.backgroundColor = .clear |
| | | view.addSubview(collectionView) |
| | | collectionView.snp.makeConstraints { make in |
| | | make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(99) |
| | | make.left.right.equalToSuperview() |
| | | make.bottom.equalToSuperview() |
| | | } |
| | | |
| | | view.layoutIfNeeded() |
| | | } |
| | | view.layoutIfNeeded() |
| | | } |
| | | |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | | if let res = Array<Any>.CalmulateCell(listen1Model.subjectList.count){ |
| | | let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | let w = (JQ_ScreenW - (collectionView.contentInset.left * 2) - (CGFloat(res.0) - 1.0) * layout.minimumInteritemSpacing) / Double(res.0) |
| | | let h = (collectionView.frame.height - (layout.minimumLineSpacing * (Double(res.1) - 1.0))) / Double(res.1) |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | | if let res = Array<Any>.CalmulateCell(listen1Model.subjectList.count){ |
| | | let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | let w = (JQ_ScreenW - (collectionView.contentInset.left * 2) - (CGFloat(res.0) - 1.0) * layout.minimumInteritemSpacing) / Double(res.0) |
| | | let h = (collectionView.frame.height - (layout.minimumLineSpacing * (Double(res.1) - 1.0))) / Double(res.1) |
| | | |
| | | if layout.itemSize != CGSize(width: w, height: h){ |
| | | layout.itemSize = CGSize(width: w, height: h) |
| | | collectionView.reloadData() |
| | | } |
| | | } |
| | | } |
| | | if layout.itemSize != CGSize(width: w, height: h){ |
| | | layout.itemSize = CGSize(width: w, height: h) |
| | | collectionView.reloadData() |
| | | } |
| | | } |
| | | } |
| | | |
| | | func startTimer(){ |
| | | if timer == nil{ |
| | | timer = Timer(timeInterval: 1.0, target: self, selector: #selector(runloopTime), userInfo: nil, repeats: true) |
| | | } |
| | | timer?.fire() |
| | | RunLoop.current.add(timer!, forMode: .common) |
| | | } |
| | | func startTimer(){ |
| | | if timer == nil{ |
| | | timer = Timer(timeInterval: 1.0, target: self, selector: #selector(runloopTime), userInfo: nil, repeats: true) |
| | | } |
| | | timer?.fire() |
| | | RunLoop.current.add(timer!, forMode: .common) |
| | | } |
| | | |
| | | @objc private func runloopTime(){ |
| | | print("进入。。。") |
| | | times -= 1 |
| | | label_hint.text = "请在\(max(times,1))s内选择答案!" |
| | | @objc private func runloopTime(){ |
| | | print("进入。。。") |
| | | times -= 1 |
| | | label_hint.text = "请在\(max(times,1))s内选择答案!" |
| | | |
| | | if times == 0{ |
| | | timer?.fireDate = .distantFuture |
| | | if let c = currentAnswer{ |
| | | answerSet.remove(c) |
| | | } |
| | | currentAnswer = answerSet.randomElement() //随机 |
| | | times = (listen1Model?.data?.time ?? 0) + 1 |
| | | timer?.fireDate = .distantPast |
| | | totalCount += 1 |
| | | rootViewModel.errorNum += 1 |
| | | label_class.text = "\(totalCount)" |
| | | } |
| | | //答题完成 |
| | | if self.answerSet.count == 0{ |
| | | timer?.invalidate() |
| | | completeQuestion() |
| | | } |
| | | } |
| | | if times == 0{ |
| | | timer?.fireDate = .distantFuture |
| | | if let c = currentAnswer{ |
| | | answerSet.remove(c) |
| | | } |
| | | currentAnswer = answerSet.randomElement() //随机 |
| | | times = (listen1Model?.data?.time ?? 0) + 1 |
| | | timer?.fireDate = .distantPast |
| | | totalCount += 1 |
| | | rootViewModel.errorNum += 1 |
| | | label_class.text = "\(totalCount)" |
| | | } |
| | | //答题完成 |
| | | if self.answerSet.count == 0{ |
| | | timer?.invalidate() |
| | | completeQuestion() |
| | | } |
| | | } |
| | | |
| | | private func answerQuestion(){ |
| | | view.layoutIfNeeded() |
| | | view.isUserInteractionEnabled = false |
| | | guard let row = viewModel.selectIndex.value?.row else { alertError(msg: "请选择");return } |
| | | private func answerQuestion(){ |
| | | view.layoutIfNeeded() |
| | | view.isUserInteractionEnabled = false |
| | | guard let row = viewModel.selectIndex.value?.row else { alertError(msg: "请选择");return } |
| | | |
| | | var answerType:Fight_lessonType = .none |
| | | var answerType:Fight_lessonType = .none |
| | | |
| | | if currentAnswer?.id == listen1Model?.subjectList[row].id{ |
| | | answerType = .success |
| | | voicePlayer.playSuccessVoice() |
| | | }else{ |
| | | answerType = .fail |
| | | voicePlayer.playFailVoice() |
| | | } |
| | | switch answerType { |
| | | case .success: |
| | | timer?.fireDate = .distantFuture |
| | | viewModel.answerType.accept(.success) |
| | | collectionView.reloadData() |
| | | if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_Game_CCell", for: viewModel.selectIndex.value!) as? ListenFight_Game_CCell{ |
| | | let newRect = cell.contentView.convert(cell.bounds, from: self.collectionView) |
| | | let x = abs(newRect.origin.x) + self.collectionView.contentInset.left + 5 |
| | | let y = abs(newRect.origin.y) + 99 + 5 |
| | | let layout = self.collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | let copyView = view_studyHandleView.copyView() |
| | | copyView.playBtn.isEnabled = false |
| | | view.addSubview(copyView) |
| | | if currentAnswer?.id == listen1Model?.subjectList[row].id{ |
| | | answerType = .success |
| | | voicePlayer.playSuccessVoice() |
| | | }else{ |
| | | answerType = .fail |
| | | voicePlayer.playFailVoice() |
| | | } |
| | | switch answerType { |
| | | case .success: |
| | | timer?.fireDate = .distantFuture |
| | | viewModel.answerType.accept(.success) |
| | | collectionView.reloadData() |
| | | if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_Game_CCell", for: viewModel.selectIndex.value!) as? ListenFight_Game_CCell{ |
| | | let newRect = cell.contentView.convert(cell.bounds, from: self.collectionView) |
| | | let x = abs(newRect.origin.x) + self.collectionView.contentInset.left + 5 |
| | | let y = abs(newRect.origin.y) + 99 + 5 |
| | | let layout = self.collectionView.collectionViewLayout as! UICollectionViewFlowLayout |
| | | let copyView = view_studyHandleView.copyView() |
| | | copyView.playBtn.isEnabled = false |
| | | view.addSubview(copyView) |
| | | |
| | | UIView.animate(withDuration: 0.5) { |
| | | copyView.frame = CGRect(x: x, y: y, width: layout.itemSize.width - 10, height: 40) |
| | | } completion: { _ in |
| | | // DispatchQueue.main.asyncAfter(deadline: .now()+0.5) { |
| | | // self.voicePlayer.playerAt(url: self.currentAnswer?.correct) |
| | | // } |
| | | UIView.animate(withDuration: 0.5) { |
| | | copyView.frame = CGRect(x: x, y: y, width: layout.itemSize.width - 10, height: 40) |
| | | } completion: { _ in |
| | | // DispatchQueue.main.asyncAfter(deadline: .now()+0.5) { |
| | | // self.voicePlayer.playerAt(url: self.currentAnswer?.correct) |
| | | // } |
| | | |
| | | if self.viewModel.answerType.value == .success{ |
| | | self.timer?.fireDate = .distantFuture |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+1) { |
| | | self.times = (self.listen1Model?.data?.time ?? 10) + 1 |
| | | self.totalCount += 1 |
| | | self.rootViewModel.correctNum += 1 |
| | | self.label_class.text = "\(self.totalCount)" |
| | | if self.viewModel.answerType.value == .success{ |
| | | self.timer?.fireDate = .distantFuture |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+1) { |
| | | self.times = (self.listen1Model?.data?.time ?? 10) + 1 |
| | | self.totalCount += 1 |
| | | self.rootViewModel.correctNum += 1 |
| | | self.label_class.text = "\(self.totalCount)" |
| | | |
| | | if let currentA = self.currentAnswer{ |
| | | self.answerSet.remove(currentA) |
| | | } |
| | | if let currentA = self.currentAnswer{ |
| | | self.answerSet.remove(currentA) |
| | | } |
| | | |
| | | self.currentAnswer = self.answerSet.randomElement() |
| | | self.viewModel.answerType.accept(.none) |
| | | print("--->下一题:\(self.currentAnswer?.id ?? 0) 剩余\(self.answerSet.count) 计数:\(self.totalCount)") |
| | | self.timer?.fireDate = .distantPast |
| | | } |
| | | } |
| | | } |
| | | } |
| | | self.currentAnswer = self.answerSet.randomElement() |
| | | self.viewModel.answerType.accept(.none) |
| | | print("--->下一题:\(self.currentAnswer?.id ?? 0) 剩余\(self.answerSet.count) 计数:\(self.totalCount)") |
| | | self.timer?.fireDate = .distantPast |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | case .fail: |
| | | rootViewModel.errorNum += 1 |
| | | totalCount += 1 |
| | | label_class.text = "\(totalCount)" |
| | | viewModel.answerType.accept(.fail) |
| | | timer?.fireDate = .distantFuture |
| | | label_hint.text = "准备听题" |
| | | //移除当前题目 |
| | | if let c = currentAnswer{ |
| | | answerSet.remove(c) |
| | | } |
| | | collectionView.reloadData() |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+1) { |
| | | self.timer?.fireDate = .distantPast |
| | | self.times = (self.listen1Model?.data?.time ?? 10) + 1 |
| | | self.currentAnswer = self.answerSet.randomElement() |
| | | } |
| | | case .none: |
| | | break |
| | | } |
| | | } |
| | | |
| | | private func completeQuestion(){ |
| | | print("答题完成") |
| | | self.label_hint.text = "已完成全部答题" |
| | | self.label_hint.snp.makeConstraints { make in |
| | | make.centerX.equalToSuperview() |
| | | make.centerY.equalTo(view_class_title) |
| | | make.height.equalTo(20) |
| | | } |
| | | case .fail: |
| | | rootViewModel.errorNum += 1 |
| | | totalCount += 1 |
| | | label_class.text = "\(totalCount)" |
| | | viewModel.answerType.accept(.fail) |
| | | timer?.fireDate = .distantFuture |
| | | label_hint.text = "准备听题" |
| | | //移除当前题目 |
| | | if let c = currentAnswer{ |
| | | answerSet.remove(c) |
| | | } |
| | | collectionView.reloadData() |
| | | DispatchQueue.main.asyncAfter(deadline: .now()+1) { |
| | | self.timer?.fireDate = .distantPast |
| | | self.times = (self.listen1Model?.data?.time ?? 10) + 1 |
| | | self.currentAnswer = self.answerSet.randomElement() |
| | | } |
| | | case .none: |
| | | break |
| | | } |
| | | } |
| | | |
| | | view_class_title.isHidden = true |
| | | view_studyHandleView.isHidden = true |
| | | private func completeQuestion(){ |
| | | print("答题完成") |
| | | self.label_hint.text = "已完成全部答题" |
| | | self.label_hint.snp.makeConstraints { make in |
| | | make.centerX.equalToSuperview() |
| | | make.centerY.equalTo(view_class_title) |
| | | make.height.equalTo(20) |
| | | } |
| | | |
| | | self.timer?.invalidate() |
| | | self.rootViewModel.answerItems[0] = self.listen1Model |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: ["gameId":listen1Model.data!.id,"gameIntegral":listen1Model.data!.integral,"complete":true]) |
| | | } |
| | | view_class_title.isHidden = true |
| | | view_studyHandleView.isHidden = true |
| | | |
| | | self.timer?.invalidate() |
| | | self.rootViewModel.answerItems[0] = self.listen1Model |
| | | NotificationCenter.default.post(name: NextLession_Noti, object: ["gameId":listen1Model.data!.id,"gameIntegral":listen1Model.data!.integral,"complete":true]) |
| | | } |
| | | } |
| | | |
| | | extension HomeListenGame_1_VC:UICollectionViewDelegate{ |
| | | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
| | | // _ = listen1Model!.subjectList[indexPath.row] |
| | | viewModel.selectIndex.accept(indexPath) |
| | | answerQuestion() |
| | | } |
| | | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
| | | // _ = listen1Model!.subjectList[indexPath.row] |
| | | viewModel.selectIndex.accept(indexPath) |
| | | answerQuestion() |
| | | } |
| | | } |
| | | |
| | | extension HomeListenGame_1_VC:UICollectionViewDataSource{ |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let model = listen1Model!.subjectList[indexPath.row] |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_Game_CCell", for: indexPath) as! ListenFight_Game_CCell |
| | | if viewModel.selectIndex.value == indexPath{ |
| | | cell.setState(state: viewModel.answerType.value) |
| | | }else{ |
| | | cell.setState(state: .none) |
| | | } |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let model = listen1Model!.subjectList[indexPath.row] |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_ListenFight_Game_CCell", for: indexPath) as! ListenFight_Game_CCell |
| | | if viewModel.selectIndex.value == indexPath{ |
| | | cell.setState(state: viewModel.answerType.value) |
| | | }else{ |
| | | cell.setState(state: .none) |
| | | } |
| | | |
| | | cell.setModel(model) |
| | | return cell |
| | | } |
| | | cell.setModel(model) |
| | | return cell |
| | | } |
| | | |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | return listen1Model?.subjectList.count ?? 0 |
| | | } |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | return listen1Model?.subjectList.count ?? 0 |
| | | } |
| | | } |
| | | |
| | | extension HomeListenGame_1_VC:VoicePlayerDelegate{ |
| | | func playComplete() { |
| | | func playComplete() { |
| | | |
| | | //防止再次播放时,误操作计算了正确率 |
| | | if viewModel.answerType.value == .none{ |
| | | view.isUserInteractionEnabled = true |
| | | } |
| | | //防止再次播放时,误操作计算了正确率 |
| | | if viewModel.answerType.value == .none{ |
| | | view.isUserInteractionEnabled = true |
| | | } |
| | | |
| | | timer?.fireDate = .distantPast ////播放中,恢复计时 |
| | | timer?.fireDate = .distantPast ////播放中,恢复计时 |
| | | |
| | | view_studyHandleView.resetView() |
| | | view_studyHandleView.resetView() |
| | | |
| | | self.label_hint.text = "准备听题" |
| | | self.label_hint.text = "准备听题" |
| | | |
| | | // if viewModel.answerType.value == .success{ |
| | | // timer?.fireDate = .distantFuture |
| | | // DispatchQueue.main.asyncAfter(deadline: .now()+1) { |
| | | // self.times = (self.listen1Model?.data?.time ?? 10) + 1 |
| | | // self.totalCount += 1 |
| | | // self.rootViewModel.correctNum += 1 |
| | | // self.label_class.text = "\(self.totalCount)" |
| | | // |
| | | // if let currentA = self.currentAnswer{ |
| | | // self.answerSet.remove(currentA) |
| | | // } |
| | | // |
| | | // self.currentAnswer = self.answerSet.randomElement() |
| | | // self.viewModel.answerType.accept(.none) |
| | | // print("--->下一题:\(self.currentAnswer?.id ?? 0) 剩余\(self.answerSet.count) 计数:\(self.totalCount)") |
| | | // self.timer?.fireDate = .distantPast |
| | | // } |
| | | // } |
| | | // if viewModel.answerType.value == .success{ |
| | | // timer?.fireDate = .distantFuture |
| | | // DispatchQueue.main.asyncAfter(deadline: .now()+1) { |
| | | // self.times = (self.listen1Model?.data?.time ?? 10) + 1 |
| | | // self.totalCount += 1 |
| | | // self.rootViewModel.correctNum += 1 |
| | | // self.label_class.text = "\(self.totalCount)" |
| | | // |
| | | // if let currentA = self.currentAnswer{ |
| | | // self.answerSet.remove(currentA) |
| | | // } |
| | | // |
| | | // self.currentAnswer = self.answerSet.randomElement() |
| | | // self.viewModel.answerType.accept(.none) |
| | | // print("--->下一题:\(self.currentAnswer?.id ?? 0) 剩余\(self.answerSet.count) 计数:\(self.totalCount)") |
| | | // self.timer?.fireDate = .distantPast |
| | | // } |
| | | // } |
| | | |
| | | |
| | | //答题完成 |
| | | if self.answerSet.count == 0{completeQuestion()} |
| | | } |
| | | |
| | | func playing() { |
| | | view.isUserInteractionEnabled = false |
| | | view_studyHandleView.playing() |
| | | timer?.fireDate = .distantFuture //播放中,暂停计时 |
| | | // label_hint.text = "播放中" |
| | | // label_hint.text = "" |
| | | label_hint.text = "请在\(times)s内选择答案!" |
| | | } |
| | | //答题完成 |
| | | if self.answerSet.count == 0{completeQuestion()} |
| | | } |
| | | |
| | | func playing() { |
| | | view.isUserInteractionEnabled = false |
| | | view_studyHandleView.playing() |
| | | timer?.fireDate = .distantFuture //播放中,暂停计时 |
| | | // label_hint.text = "播放中" |
| | | // label_hint.text = "" |
| | | label_hint.text = "请在\(times)s内选择答案!" |
| | | } |
| | | |
| | | } |
| | |
| | | |
| | | class HomeListenSubVC: BaseVC { |
| | | |
| | | private var page:Int! |
| | | private var quarter:Int! |
| | | private var week:Int! |
| | | private(set) var tableView:UITableView! |
| | | var studyScheduleModel:StudyScheduleModel? |
| | | private var page:Int! |
| | | private var quarter:Int! |
| | | private var week:Int! |
| | | private(set) var tableView:UITableView! |
| | | var studyScheduleModel:StudyScheduleModel? |
| | | |
| | | required init(page:Int,quarter:Int,week:Int,studyScheduleModel:StudyScheduleModel) { |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.page = page |
| | | self.quarter = quarter |
| | | self.week = week |
| | | self.studyScheduleModel = studyScheduleModel |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | required init(page:Int,quarter:Int,week:Int,studyScheduleModel:StudyScheduleModel) { |
| | | super.init(nibName: nil, bundle: nil) |
| | | self.page = page |
| | | self.quarter = quarter |
| | | self.week = week |
| | | self.studyScheduleModel = studyScheduleModel |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | navigationItem.titleView = UIView() |
| | | navigationItem.titleView = UIView() |
| | | } |
| | | |
| | | override func setUI() { |
| | | super.setUI() |
| | | tableView = UITableView(frame: .zero, style: .plain) |
| | | tableView.delegate = self |
| | | tableView.dataSource = self |
| | | tableView.separatorStyle = .none |
| | | tableView.showsVerticalScrollIndicator = false |
| | | tableView.showsHorizontalScrollIndicator = false |
| | | tableView.backgroundColor = Config.ThemeBGColor |
| | | tableView.register(UINib(nibName: "HomeListen_process_TCell", bundle: nil), forCellReuseIdentifier: "_HomeListen_process_TCell") |
| | | tableView.register(UINib(nibName: "HomeListen_item_TCell", bundle: nil), forCellReuseIdentifier: "_HomeListen_item_TCell") |
| | | view.addSubview(tableView) |
| | | tableView.snp.makeConstraints { make in |
| | | make.edges.equalToSuperview() |
| | | } |
| | | } |
| | | override func setUI() { |
| | | super.setUI() |
| | | tableView = UITableView(frame: .zero, style: .plain) |
| | | tableView.delegate = self |
| | | tableView.dataSource = self |
| | | tableView.separatorStyle = .none |
| | | tableView.showsVerticalScrollIndicator = false |
| | | tableView.showsHorizontalScrollIndicator = false |
| | | tableView.backgroundColor = Config.ThemeBGColor |
| | | tableView.register(UINib(nibName: "HomeListen_process_TCell", bundle: nil), forCellReuseIdentifier: "_HomeListen_process_TCell") |
| | | tableView.register(UINib(nibName: "HomeListen_item_TCell", bundle: nil), forCellReuseIdentifier: "_HomeListen_item_TCell") |
| | | view.addSubview(tableView) |
| | | tableView.snp.makeConstraints { make in |
| | | make.edges.equalToSuperview() |
| | | } |
| | | } |
| | | |
| | | func jumpAt(listenType:ListenType){ |
| | | let row = listenType.rawValue - 1 |
| | | let jumpIndex:IndexPath = IndexPath(row: row, section: 1) |
| | | tableView(self.tableView, didSelectRowAt: jumpIndex) |
| | | } |
| | | func jumpAt(listenType:ListenType){ |
| | | let row = listenType.rawValue - 1 |
| | | let jumpIndex:IndexPath = IndexPath(row: row, section: 1) |
| | | tableView(self.tableView, didSelectRowAt: jumpIndex) |
| | | } |
| | | } |
| | | |
| | | extension HomeListenSubVC:UITableViewDelegate{ |
| | | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
| | | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
| | | |
| | | if page <= 4 && indexPath.section == 0{return} |
| | | if page <= 4 && indexPath.section == 0{return} |
| | | |
| | | let day = page + 1 |
| | | sceneDelegate?.startTimer() |
| | | let day = page + 1 |
| | | sceneDelegate?.startTimer() |
| | | |
| | | if page <= 4{ |
| | | if indexPath.row == 0{ |
| | | Services.teamSchedule(type: .lesson1, week: week, day: day).subscribe(onNext: {[weak self] teamSchedule in |
| | | guard let weakSelf = self else { return } |
| | | //听音选图 |
| | | Services.listenSelectPicture(day:day, quarter: weakSelf.quarter, week: weakSelf.week).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson1,quarter:weakSelf.quarter,week: weakSelf.week,day:day) |
| | | fightVC.title = ListenType.lesson1.rawTitle |
| | | fightVC.teamScheduleModel = teamSchedule.data |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc:fightVC) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | if page <= 4{ |
| | | if indexPath.row == 0{ |
| | | Services.teamSchedule(type: .lesson1, week: week, day: day).subscribe(onNext: {[weak self] teamSchedule in |
| | | guard let weakSelf = self else { return } |
| | | //听音选图 |
| | | Services.listenSelectPicture(day:day, quarter: weakSelf.quarter, week: weakSelf.week).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson1,quarter:weakSelf.quarter,week: weakSelf.week,day:day) |
| | | fightVC.title = ListenType.lesson1.rawTitle |
| | | fightVC.teamScheduleModel = teamSchedule.data |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc:fightVC) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | if indexPath.row == 1{ |
| | | //看图选音 |
| | | Services.teamSchedule(type: .lesson2, week: week, day: day).subscribe(onNext: {[weak self] teamSchedule in |
| | | guard let weakSelf = self else { return } |
| | | Services.pictureSelectVoice(day: day, quarter: weakSelf.quarter, week: weakSelf.week).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson2,quarter:weakSelf.quarter,week: weakSelf.week,day:day) |
| | | fightVC.title = ListenType.lesson2.rawTitle |
| | | fightVC.teamScheduleModel = teamSchedule.data |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc:fightVC) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | }).disposed(by: disposeBag) |
| | | if indexPath.row == 1{ |
| | | //看图选音 |
| | | Services.teamSchedule(type: .lesson2, week: week, day: day).subscribe(onNext: {[weak self] teamSchedule in |
| | | guard let weakSelf = self else { return } |
| | | Services.pictureSelectVoice(day: day, quarter: weakSelf.quarter, week: weakSelf.week).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson2,quarter:weakSelf.quarter,week: weakSelf.week,day:day) |
| | | fightVC.title = ListenType.lesson2.rawTitle |
| | | fightVC.teamScheduleModel = teamSchedule.data |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc:fightVC) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | }).disposed(by: disposeBag) |
| | | |
| | | } |
| | | } |
| | | |
| | | if indexPath.row == 2{ |
| | | //归纳排除 |
| | | Services.teamSchedule(type: .lesson3, week: week, day: day).subscribe(onNext: {[weak self] teamSchedule in |
| | | guard let weakSelf = self else { return } |
| | | Services.induceExclude(day: day, quarter: weakSelf.quarter, week: weakSelf.week).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson3,quarter:weakSelf.quarter,week: weakSelf.week,day:day) |
| | | fightVC.title = ListenType.lesson3.rawTitle |
| | | fightVC.teamScheduleModel = teamSchedule.data |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc: fightVC) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | if indexPath.row == 2{ |
| | | //归纳排除 |
| | | Services.teamSchedule(type: .lesson3, week: week, day: day).subscribe(onNext: {[weak self] teamSchedule in |
| | | guard let weakSelf = self else { return } |
| | | Services.induceExclude(day: day, quarter: weakSelf.quarter, week: weakSelf.week).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson3,quarter:weakSelf.quarter,week: weakSelf.week,day:day) |
| | | fightVC.title = ListenType.lesson3.rawTitle |
| | | fightVC.teamScheduleModel = teamSchedule.data |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc: fightVC) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | if indexPath.row == 3{ |
| | | //有问有答 |
| | | Services.teamSchedule(type: .lesson4, week: week, day: day).subscribe(onNext: {[weak self] teamSchedule in |
| | | guard let weakSelf = self else { return } |
| | | Services.questionsAndAnswers(day: day, quarter: weakSelf.quarter, week: weakSelf.week).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson4,quarter:weakSelf.quarter,week: weakSelf.week,day:day) |
| | | fightVC.title = ListenType.lesson4.rawTitle |
| | | fightVC.teamScheduleModel = teamSchedule.data |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc: fightVC) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | if indexPath.row == 3{ |
| | | //有问有答 |
| | | Services.teamSchedule(type: .lesson4, week: week, day: day).subscribe(onNext: {[weak self] teamSchedule in |
| | | guard let weakSelf = self else { return } |
| | | Services.questionsAndAnswers(day: day, quarter: weakSelf.quarter, week: weakSelf.week).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson4,quarter:weakSelf.quarter,week: weakSelf.week,day:day) |
| | | fightVC.title = ListenType.lesson4.rawTitle |
| | | fightVC.teamScheduleModel = teamSchedule.data |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc: fightVC) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | if indexPath.row == 4{ |
| | | //音图相配 |
| | | Services.teamSchedule(type: .lesson5, week: week, day: day).subscribe(onNext: {[weak self] teamSchedule in |
| | | guard let weakSelf = self else { return } |
| | | Services.pictureMateVoice(day: day, quarter: weakSelf.quarter, week: weakSelf.week).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson5,quarter:weakSelf.quarter,week: weakSelf.week,day:day) |
| | | fightVC.title = ListenType.lesson5.rawTitle |
| | | fightVC.teamScheduleModel = teamSchedule.data |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc: fightVC) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | } |
| | | if indexPath.row == 4{ |
| | | //音图相配 |
| | | Services.teamSchedule(type: .lesson5, week: week, day: day).subscribe(onNext: {[weak self] teamSchedule in |
| | | guard let weakSelf = self else { return } |
| | | Services.pictureMateVoice(day: day, quarter: weakSelf.quarter, week: weakSelf.week).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson5,quarter:weakSelf.quarter,week: weakSelf.week,day:day) |
| | | fightVC.title = ListenType.lesson5.rawTitle |
| | | fightVC.teamScheduleModel = teamSchedule.data |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc: fightVC) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | } |
| | | |
| | | //自主游戏 |
| | | if page == 5{ |
| | | if indexPath.row == 0{ |
| | | let fightVC = HomeListenFightVC(listenType: .game1,quarter: quarter,week: week,day: day) |
| | | fightVC.title = ListenType.game1.rawTitle |
| | | fightVC.studyScheduleModel = studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc:fightVC) |
| | | } |
| | | if indexPath.row == 1{ |
| | | Services.gameMemory(quarter: quarter, week: week).subscribe(onNext: {[weak self]result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .game2,quarter: weakSelf.quarter,week: weakSelf.week,day: day) |
| | | fightVC.title = ListenType.game2.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | } |
| | | //自主游戏 |
| | | if page == 5{ |
| | | if indexPath.row == 0{ |
| | | let fightVC = HomeListenFightVC(listenType: .game1,quarter: quarter,week: week,day: day) |
| | | fightVC.title = ListenType.game1.rawTitle |
| | | fightVC.studyScheduleModel = studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc:fightVC) |
| | | } |
| | | if indexPath.row == 1{ |
| | | Services.gameMemory(quarter: quarter, week: week).subscribe(onNext: {[weak self]result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .game2,quarter: weakSelf.quarter,week: weakSelf.week,day: day) |
| | | fightVC.title = ListenType.game2.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | } |
| | | |
| | | //听故事 |
| | | if page == 6{ |
| | | if indexPath.row == 0{ |
| | | Services.lookpictureDbu(quarter: quarter, week: week).subscribe(onNext: {[weak self]result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .story1,quarter: weakSelf.quarter,week: weakSelf.week,day: day) |
| | | fightVC.title = ListenType.story1.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | if indexPath.row == 1{ |
| | | Services.frameworkMemory(quarter: quarter, week: week).subscribe(onNext: {[weak self]result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .story2,quarter: weakSelf.quarter,week: weakSelf.week,day: day) |
| | | fightVC.title = ListenType.story2.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | } |
| | | } |
| | | //听故事 |
| | | if page == 6{ |
| | | if indexPath.row == 0{ |
| | | Services.lookpictureDbu(quarter: quarter, week: week).subscribe(onNext: {[weak self]result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .story1,quarter: weakSelf.quarter,week: weakSelf.week,day: day) |
| | | fightVC.title = ListenType.story1.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | if indexPath.row == 1{ |
| | | Services.frameworkMemory(quarter: quarter, week: week).subscribe(onNext: {[weak self]result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .story2,quarter: weakSelf.quarter,week: weakSelf.week,day: day) |
| | | fightVC.title = ListenType.story2.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = self?.studyScheduleModel |
| | | JQ_currentViewController().jq_push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension HomeListenSubVC:UITableViewDataSource{ |
| | | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
| | | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
| | | |
| | | if page <= 4{ |
| | | if section == 0{return 1} |
| | | return 5 |
| | | }else{ |
| | | return 2 |
| | | } |
| | | } |
| | | if page <= 4{ |
| | | if section == 0{return 1} |
| | | return 5 |
| | | }else{ |
| | | return 2 |
| | | } |
| | | } |
| | | |
| | | func numberOfSections(in tableView: UITableView) -> Int { |
| | | if page <= 4{ |
| | | return 2 |
| | | } |
| | | return 1 |
| | | } |
| | | func numberOfSections(in tableView: UITableView) -> Int { |
| | | if page <= 4{ |
| | | return 2 |
| | | } |
| | | return 1 |
| | | } |
| | | |
| | | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
| | | if indexPath.section == 0 && page <= 4{ |
| | | let cell = tableView.dequeueReusableCell(withIdentifier: "_HomeListen_process_TCell", for: indexPath) as! HomeListen_process_TCell |
| | | cell.studyScheduleModel = studyScheduleModel |
| | | cell.label_currentWeek.text = "当前周目:\(week.jq_cn)周目" |
| | | return cell |
| | | }else{ |
| | | let cell = tableView.dequeueReusableCell(withIdentifier: "_HomeListen_item_TCell", for: indexPath) as! HomeListen_item_TCell |
| | | cell.label_title.text = "\(indexPath.row + 1)" |
| | | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
| | | if indexPath.section == 0 && page <= 4{ |
| | | let cell = tableView.dequeueReusableCell(withIdentifier: "_HomeListen_process_TCell", for: indexPath) as! HomeListen_process_TCell |
| | | cell.studyScheduleModel = studyScheduleModel |
| | | cell.label_currentWeek.text = "当前周目:\(week.jq_cn)周目" |
| | | return cell |
| | | }else{ |
| | | let cell = tableView.dequeueReusableCell(withIdentifier: "_HomeListen_item_TCell", for: indexPath) as! HomeListen_item_TCell |
| | | cell.label_title.text = "\(indexPath.row + 1)" |
| | | |
| | | if page <= 4{ |
| | | cell.view_bg1.isHidden = true |
| | | switch indexPath.row { |
| | | case 0: |
| | | cell.label_title.text = "自主学习1-听音选图" |
| | | cell.view_bg2.backgroundColor = UIColor(hexString: "#6EC3FF") |
| | | if page <= 4{ |
| | | cell.view_bg1.isHidden = true |
| | | switch indexPath.row { |
| | | case 0: |
| | | cell.label_title.text = "自主学习1-听音选图" |
| | | cell.view_bg2.backgroundColor = UIColor(hexString: "#6EC3FF") |
| | | |
| | | if studyScheduleModel?.day == (page + 1){ |
| | | cell.setProgress(progress: studyScheduleModel?.listen ?? 0) |
| | | }else if (studyScheduleModel?.day ?? 0) > (page+1){ |
| | | cell.setProgress(progress: 100) |
| | | }else{ |
| | | cell.setProgress(progress: 0) |
| | | } |
| | | case 1: |
| | | cell.label_title.text = "自主学习2-看图选音" |
| | | cell.view_bg2.backgroundColor = UIColor(hexString: "#FF9A85") |
| | | if studyScheduleModel?.day == (page + 1){ |
| | | cell.setProgress(progress: studyScheduleModel?.listen ?? 0) |
| | | }else if (studyScheduleModel?.day ?? 0) > (page+1){ |
| | | cell.setProgress(progress: 100) |
| | | }else{ |
| | | cell.setProgress(progress: 0) |
| | | } |
| | | case 1: |
| | | cell.label_title.text = "自主学习2-看图选音" |
| | | cell.view_bg2.backgroundColor = UIColor(hexString: "#FF9A85") |
| | | |
| | | if studyScheduleModel?.day == (page + 1){ |
| | | cell.setProgress(progress: studyScheduleModel?.look ?? 0) |
| | | }else if (studyScheduleModel?.day ?? 0) > (page+1){ |
| | | cell.setProgress(progress: 100) |
| | | }else{ |
| | | cell.setProgress(progress: 0) |
| | | } |
| | | case 2: |
| | | cell.label_title.text = "自主学习3-归纳排除" |
| | | cell.view_bg2.backgroundColor = UIColor(hexString: "#28C8C5") |
| | | if studyScheduleModel?.day == (page + 1){ |
| | | cell.setProgress(progress: studyScheduleModel?.look ?? 0) |
| | | }else if (studyScheduleModel?.day ?? 0) > (page+1){ |
| | | cell.setProgress(progress: 100) |
| | | }else{ |
| | | cell.setProgress(progress: 0) |
| | | } |
| | | case 2: |
| | | cell.label_title.text = "自主学习3-归纳排除" |
| | | cell.view_bg2.backgroundColor = UIColor(hexString: "#28C8C5") |
| | | |
| | | |
| | | if studyScheduleModel?.day == (page + 1){ |
| | | cell.setProgress(progress: studyScheduleModel?.induction ?? 0) |
| | | }else if (studyScheduleModel?.day ?? 0) > (page+1){ |
| | | cell.setProgress(progress: 100) |
| | | }else{ |
| | | cell.setProgress(progress: 0) |
| | | } |
| | | case 3: |
| | | cell.label_title.text = "自主学习4-有问有答" |
| | | cell.view_bg2.backgroundColor = UIColor(hexString: "#F8A169") |
| | | if studyScheduleModel?.day == (page + 1){ |
| | | cell.setProgress(progress: studyScheduleModel?.induction ?? 0) |
| | | }else if (studyScheduleModel?.day ?? 0) > (page+1){ |
| | | cell.setProgress(progress: 100) |
| | | }else{ |
| | | cell.setProgress(progress: 0) |
| | | } |
| | | case 3: |
| | | cell.label_title.text = "自主学习4-有问有答" |
| | | cell.view_bg2.backgroundColor = UIColor(hexString: "#F8A169") |
| | | |
| | | |
| | | if studyScheduleModel?.day == (page + 1){ |
| | | cell.setProgress(progress: studyScheduleModel?.answer ?? 0) |
| | | }else if (studyScheduleModel?.day ?? 0) > (page+1){ |
| | | cell.setProgress(progress: 100) |
| | | }else{ |
| | | cell.setProgress(progress: 0) |
| | | } |
| | | case 4: |
| | | cell.label_title.text = "自主学习5-音图相配" |
| | | cell.view_bg2.backgroundColor = UIColor(hexString: "#92CADB") |
| | | if studyScheduleModel?.day == (page + 1){ |
| | | cell.setProgress(progress: studyScheduleModel?.answer ?? 0) |
| | | }else if (studyScheduleModel?.day ?? 0) > (page+1){ |
| | | cell.setProgress(progress: 100) |
| | | }else{ |
| | | cell.setProgress(progress: 0) |
| | | } |
| | | case 4: |
| | | cell.label_title.text = "自主学习5-音图相配" |
| | | cell.view_bg2.backgroundColor = UIColor(hexString: "#92CADB") |
| | | |
| | | if studyScheduleModel?.day == (page + 1){ |
| | | cell.setProgress(progress: studyScheduleModel?.pair ?? 0) |
| | | }else if (studyScheduleModel?.day ?? 0) > (page+1){ |
| | | cell.setProgress(progress: 100) |
| | | }else{ |
| | | cell.setProgress(progress: 0) |
| | | } |
| | | default:break |
| | | } |
| | | } |
| | | if studyScheduleModel?.day == (page + 1){ |
| | | cell.setProgress(progress: studyScheduleModel?.pair ?? 0) |
| | | }else if (studyScheduleModel?.day ?? 0) > (page+1){ |
| | | cell.setProgress(progress: 100) |
| | | }else{ |
| | | cell.setProgress(progress: 0) |
| | | } |
| | | default:break |
| | | } |
| | | } |
| | | |
| | | if page == 5{ |
| | | cell.view_bg2.isHidden = true |
| | | cell.view_state.isHidden = true |
| | | switch indexPath.row { |
| | | case 0: |
| | | cell.label_title.text = "自主游戏1-超级听力" |
| | | cell.view_bg1.backgroundColor = UIColor(hexString: "#6EC3FF") |
| | | case 1: |
| | | cell.label_title.text = "自主游戏2-超级记忆" |
| | | cell.view_bg1.backgroundColor = UIColor(hexString: "#FF9A85") |
| | | default:break |
| | | } |
| | | } |
| | | if page == 5{ |
| | | cell.view_bg2.isHidden = true |
| | | cell.view_state.isHidden = true |
| | | switch indexPath.row { |
| | | case 0: |
| | | cell.label_title.text = "自主游戏1-超级听力" |
| | | cell.view_bg1.backgroundColor = UIColor(hexString: "#6EC3FF") |
| | | case 1: |
| | | cell.label_title.text = "自主游戏2-超级记忆" |
| | | cell.view_bg1.backgroundColor = UIColor(hexString: "#FF9A85") |
| | | default:break |
| | | } |
| | | } |
| | | |
| | | if page == 6{ |
| | | cell.view_bg2.isHidden = true |
| | | cell.view_state.isHidden = true |
| | | switch indexPath.row { |
| | | case 0: |
| | | cell.label_title.text = "自主故事1-看图配音" |
| | | cell.view_bg1.backgroundColor = UIColor(hexString: "#6EC3FF") |
| | | case 1: |
| | | cell.label_title.text = "自主故事2-框架记忆" |
| | | cell.view_bg1.backgroundColor = UIColor(hexString: "#FF9A85") |
| | | default:break |
| | | } |
| | | } |
| | | if page == 6{ |
| | | cell.view_bg2.isHidden = true |
| | | cell.view_state.isHidden = true |
| | | switch indexPath.row { |
| | | case 0: |
| | | cell.label_title.text = "自主故事1-看图配音" |
| | | cell.view_bg1.backgroundColor = UIColor(hexString: "#6EC3FF") |
| | | case 1: |
| | | cell.label_title.text = "自主故事2-框架记忆" |
| | | cell.view_bg1.backgroundColor = UIColor(hexString: "#FF9A85") |
| | | default:break |
| | | } |
| | | } |
| | | |
| | | return cell |
| | | } |
| | | } |
| | | return cell |
| | | } |
| | | } |
| | | |
| | | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { |
| | | if page <= 5{ |
| | | if indexPath.section == 0{return 145.5} |
| | | return 127.5 |
| | | }else{ |
| | | return 127.5 |
| | | } |
| | | } |
| | | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { |
| | | if page <= 5{ |
| | | if indexPath.section == 0{return 145.5} |
| | | return 127.5 |
| | | }else{ |
| | | return 127.5 |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | self.pageVC.reloadData() |
| | | |
| | | #if !DEBUG |
| | | if limitDay == 6{ |
| | | self.pageVC.scroll(toPage: 4, animation: true) |
| | | }else{ |
| | | self.pageVC.scroll(toPage: self.limitDay - 1, animation: true) |
| | | } |
| | | #endif |
| | | } |
| | | |
| | | override func setUI() { |
| | |
| | | @IBOutlet weak var view_state: UIView! |
| | | @IBOutlet weak var view_handle: UIView! |
| | | @IBOutlet weak var btn_isAnswer: UIButton! |
| | | @IBOutlet weak var img_play: UIImageView! |
| | | @IBOutlet weak var img_play: UIButton! |
| | | @IBOutlet weak var btn_playing: UIButton! |
| | | |
| | | var voiceUrl:String? |
| | | var isCopy:Bool = false |
| | | var playAtClouse:((Int)->Void)? |
| | | var isplayend:Bool = false |
| | | |
| | | override func awakeFromNib() { |
| | | super.awakeFromNib() |
| | | img_state.alpha = 0 |
| | | img_play.alpha = 0 |
| | | |
| | | view_handle.backgroundColor = .white |
| | | btn_isAnswer.setImage(UIImage(named: "icon_answer"), for: .normal) |
| | | img_play.setImage(UIImage(named: "icon_play"), for: .normal) |
| | | btn_playing.setImage(UIImage(named: "icon_play_1"), for: .normal) |
| | | |
| | | |
| | | view_handle.isUserInteractionEnabled = true |
| | | let tap = UITapGestureRecognizer(target: self, action: #selector(playAction)) |
| | | view_handle.addGestureRecognizer(tap) |
| | | |
| | | // VoicePlayer.share().playEnd { |
| | | // if self.isCopy{ |
| | | // self.img_play.alpha = 1 |
| | |
| | | } |
| | | |
| | | func isPlaying(){ |
| | | btn_playing.setImage(UIImage(named: "icon_playing"), for: .normal) |
| | | isplayend = true |
| | | btn_playing.setImage(UIImage(named: "icon_playing")?.themeGreen, for: .normal) |
| | | btn_isAnswer.isHidden = true |
| | | img_play.isHidden = true |
| | | } |
| | | |
| | | func playEnd(){ |
| | | btn_isAnswer.isHidden = false |
| | | btn_playing.setImage(UIImage(named: "icon_play_1"), for: .normal) |
| | | btn_playing.setImage(UIImage(named: "icon_play_1"), for: .normal) |
| | | } |
| | | |
| | | @IBAction func playAction(_ sender: UIButton) { |
| | | @objc private func playAction() { |
| | | if let url = voiceUrl{ |
| | | VoicePlayer.share().playerAt(url: url) |
| | | img_play.alpha = 0 |
| | | playAtClouse?(self.tag) |
| | | btn_playing.setImage(UIImage(named: "icon_playing"), for: .normal) |
| | | btn_playing.setImage(UIImage(named: "icon_playing")?.themeGreen, for: .normal) |
| | | btn_isAnswer.isHidden = true |
| | | img_play.isHidden = true |
| | | |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> |
| | | <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> |
| | | <device id="ipad10_9rounded" orientation="portrait" layout="fullscreen" appearance="light"/> |
| | | <dependencies> |
| | | <deployment identifier="iOS"/> |
| | | <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/> |
| | | <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/> |
| | | <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> |
| | | </dependencies> |
| | | <objects> |
| | |
| | | <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="qsr-6I-w3R"> |
| | | <rect key="frame" x="0.0" y="0.0" width="152" height="52"/> |
| | | <subviews> |
| | | <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="otl-1Z-qED"> |
| | | <rect key="frame" x="15.5" y="15" width="28" height="22"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_answer"/> |
| | | </button> |
| | | <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mba-RI-50M"> |
| | | <rect key="frame" x="62.5" y="12.5" width="27" height="27"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_play_1"/> |
| | | </button> |
| | | <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon_fail" translatesAutoresizingMaskIntoConstraints="NO" id="mkh-g4-79e"> |
| | | <rect key="frame" x="100.5" y="10" width="32" height="32"/> |
| | | <rect key="frame" x="60" y="10" width="32" height="32"/> |
| | | <constraints> |
| | | <constraint firstAttribute="height" constant="32" id="unt-83-tZW"/> |
| | | <constraint firstAttribute="width" constant="32" id="yDR-8r-kHx"/> |
| | | </constraints> |
| | | </imageView> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tfO-7L-o0T"> |
| | | <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="x83-pW-htY"> |
| | | <rect key="frame" x="0.0" y="0.0" width="152" height="52"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <connections> |
| | | <action selector="playAction:" destination="iN0-l3-epB" eventType="touchUpInside" id="mAa-LF-0SJ"/> |
| | | </connections> |
| | | </button> |
| | | <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon_play" translatesAutoresizingMaskIntoConstraints="NO" id="YeU-8E-35u"> |
| | | <rect key="frame" x="102" y="10" width="32" height="32"/> |
| | | </imageView> |
| | | <subviews> |
| | | <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="otl-1Z-qED"> |
| | | <rect key="frame" x="0.0" y="0.0" width="50.5" height="52"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_answer"/> |
| | | </button> |
| | | <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mba-RI-50M"> |
| | | <rect key="frame" x="50.5" y="0.0" width="51" height="52"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_play_1"/> |
| | | </button> |
| | | <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="KTh-43-lUh"> |
| | | <rect key="frame" x="101.5" y="0.0" width="50.5" height="52"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_play"/> |
| | | </button> |
| | | </subviews> |
| | | </stackView> |
| | | </subviews> |
| | | <color key="backgroundColor" red="0.25490196079999999" green="0.63529411759999999" blue="0.92156862750000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> |
| | | <constraints> |
| | | <constraint firstItem="mba-RI-50M" firstAttribute="leading" secondItem="otl-1Z-qED" secondAttribute="trailing" constant="19" id="0nx-t9-1zV"/> |
| | | <constraint firstItem="YeU-8E-35u" firstAttribute="centerY" secondItem="tfO-7L-o0T" secondAttribute="centerY" id="2DM-IT-SGr"/> |
| | | <constraint firstAttribute="width" constant="152" id="3Zx-vg-H2U"/> |
| | | <constraint firstAttribute="bottom" secondItem="tfO-7L-o0T" secondAttribute="bottom" id="8ro-2n-YNH"/> |
| | | <constraint firstAttribute="trailing" secondItem="tfO-7L-o0T" secondAttribute="trailing" id="A2h-po-R4k"/> |
| | | <constraint firstAttribute="trailing" secondItem="YeU-8E-35u" secondAttribute="trailing" constant="18" id="DAX-dg-Ase"/> |
| | | <constraint firstItem="mba-RI-50M" firstAttribute="centerY" secondItem="otl-1Z-qED" secondAttribute="centerY" id="FTc-eb-PWk"/> |
| | | <constraint firstItem="tfO-7L-o0T" firstAttribute="top" secondItem="qsr-6I-w3R" secondAttribute="top" id="HqC-Ia-1OU"/> |
| | | <constraint firstItem="mba-RI-50M" firstAttribute="centerX" secondItem="tfO-7L-o0T" secondAttribute="centerX" id="M4G-fi-i36"/> |
| | | <constraint firstItem="mkh-g4-79e" firstAttribute="centerY" secondItem="otl-1Z-qED" secondAttribute="centerY" id="N6f-d4-80e"/> |
| | | <constraint firstItem="tfO-7L-o0T" firstAttribute="leading" secondItem="qsr-6I-w3R" secondAttribute="leading" id="QfH-1Q-LyV"/> |
| | | <constraint firstItem="mkh-g4-79e" firstAttribute="leading" secondItem="mba-RI-50M" secondAttribute="trailing" constant="11" id="a9o-Lq-9SP"/> |
| | | <constraint firstItem="mba-RI-50M" firstAttribute="centerY" secondItem="tfO-7L-o0T" secondAttribute="centerY" id="gPd-3s-zX2"/> |
| | | <constraint firstItem="otl-1Z-qED" firstAttribute="centerY" secondItem="qsr-6I-w3R" secondAttribute="centerY" id="v0P-gn-zbN"/> |
| | | <constraint firstItem="mkh-g4-79e" firstAttribute="centerY" secondItem="x83-pW-htY" secondAttribute="centerY" id="FBf-rK-ic5"/> |
| | | <constraint firstAttribute="trailing" secondItem="x83-pW-htY" secondAttribute="trailing" id="Q2V-v1-cdD"/> |
| | | <constraint firstItem="x83-pW-htY" firstAttribute="top" secondItem="qsr-6I-w3R" secondAttribute="top" id="SgE-1G-uBq"/> |
| | | <constraint firstItem="x83-pW-htY" firstAttribute="leading" secondItem="qsr-6I-w3R" secondAttribute="leading" id="Wre-wg-0ii"/> |
| | | <constraint firstAttribute="bottom" secondItem="x83-pW-htY" secondAttribute="bottom" id="kkD-H8-FcP"/> |
| | | <constraint firstItem="mkh-g4-79e" firstAttribute="centerX" secondItem="x83-pW-htY" secondAttribute="centerX" id="ree-1r-FCJ"/> |
| | | </constraints> |
| | | <userDefinedRuntimeAttributes> |
| | | <userDefinedRuntimeAttribute type="boolean" keyPath="ld_maskToBoundsXIB" value="YES"/> |
| | |
| | | <outlet property="btn_choose" destination="v7f-gv-EWR" id="wFE-Gp-xAd"/> |
| | | <outlet property="btn_isAnswer" destination="otl-1Z-qED" id="0Fg-A8-9dl"/> |
| | | <outlet property="btn_playing" destination="mba-RI-50M" id="Gmt-aT-rn4"/> |
| | | <outlet property="img_play" destination="YeU-8E-35u" id="Xvc-ls-0sz"/> |
| | | <outlet property="img_play" destination="KTh-43-lUh" id="7GM-KZ-Ikc"/> |
| | | <outlet property="img_state" destination="mkh-g4-79e" id="IzD-w8-jjK"/> |
| | | <outlet property="view_handle" destination="qsr-6I-w3R" id="ptU-09-NIc"/> |
| | | <outlet property="view_state" destination="Gbs-f8-132" id="Swt-nY-Bpv"/> |
| | |
| | | <resources> |
| | | <image name="btn_radio" width="52" height="52"/> |
| | | <image name="btn_radio_u" width="52" height="52"/> |
| | | <image name="icon_answer" width="28" height="14"/> |
| | | <image name="icon_answer" width="42" height="21"/> |
| | | <image name="icon_fail" width="80" height="80"/> |
| | | <image name="icon_play" width="32" height="32"/> |
| | | <image name="icon_play_1" width="27" height="27"/> |
| | | <image name="icon_play" width="45" height="45"/> |
| | | <image name="icon_play_1" width="28.5" height="30"/> |
| | | </resources> |
| | | </document> |
| | |
| | | let StudyCompleteCoinUpdate_Noti = Notification.Name.init("StudyCompleteCoinUpdate_Noti") |
| | | |
| | | class HomeStudyCompleteVC: BaseVC { |
| | | @IBOutlet weak var label_coin: UILabel! |
| | | @IBOutlet weak var label_correctNum: UILabel! |
| | | @IBOutlet weak var label_title_correctNum: UILabel! |
| | | @IBOutlet weak var label_totalNum: UILabel! |
| | | @IBOutlet weak var label_title_totalNum: UILabel! |
| | | @IBOutlet weak var label_errorNum: UILabel! |
| | | @IBOutlet weak var label_title_errorNum: UILabel! |
| | | @IBOutlet weak var label_ratioNum: UILabel! |
| | | @IBOutlet weak var btn_next: UIButton! |
| | | @IBOutlet weak var stackView: UIStackView! |
| | | @IBOutlet weak var btn_back: UIButton! |
| | | @IBOutlet weak var label_coin: UILabel! |
| | | @IBOutlet weak var label_correctNum: UILabel! |
| | | @IBOutlet weak var label_title_correctNum: UILabel! |
| | | @IBOutlet weak var label_totalNum: UILabel! |
| | | @IBOutlet weak var label_title_totalNum: UILabel! |
| | | @IBOutlet weak var label_errorNum: UILabel! |
| | | @IBOutlet weak var label_title_errorNum: UILabel! |
| | | @IBOutlet weak var label_ratioNum: UILabel! |
| | | @IBOutlet weak var btn_next: UIButton! |
| | | @IBOutlet weak var stackView: UIStackView! |
| | | @IBOutlet weak var btn_back: UIButton! |
| | | |
| | | // private var totalCoin:Int = 0 |
| | | private var totalNum:Int? //总题目数量 |
| | | // private var totalCoin:Int = 0 |
| | | private var totalNum:Int? //总题目数量 |
| | | |
| | | var viewModel:HomeListenFightViewModel! |
| | | var studyScheduleModel:StudyScheduleModel! |
| | | var viewModel:HomeListenFightViewModel! |
| | | var studyScheduleModel:StudyScheduleModel! |
| | | |
| | | required init(totalNum:Int? = nil,viewModel:HomeListenFightViewModel,studyScheduleModel:StudyScheduleModel){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | // self.totalCoin = totalCoin |
| | | self.totalNum = totalNum |
| | | self.viewModel = viewModel |
| | | self.studyScheduleModel = studyScheduleModel |
| | | } |
| | | required init(totalNum:Int? = nil,viewModel:HomeListenFightViewModel,studyScheduleModel:StudyScheduleModel){ |
| | | super.init(nibName: nil, bundle: nil) |
| | | // self.totalCoin = totalCoin |
| | | self.totalNum = totalNum |
| | | self.viewModel = viewModel |
| | | self.studyScheduleModel = studyScheduleModel |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | |
| | | navigationController?.viewControllers.removeAll(where: { vc in |
| | | return vc is HomeListenFightVC |
| | | }) |
| | | navigationController?.viewControllers.removeAll(where: { vc in |
| | | return vc is HomeListenFightVC |
| | | }) |
| | | |
| | | |
| | | |
| | | yy_popBlock = {[weak self] () in |
| | | self?.backAction() |
| | | } |
| | | yy_popBlock = {[weak self] () in |
| | | self?.backAction() |
| | | } |
| | | |
| | | label_coin.text = "恭喜您,已完成全部答题!获得0积分!" |
| | | label_correctNum.text = "\(viewModel.correctNum)次" |
| | | label_errorNum.text = "\(viewModel.errorNum)次" |
| | | label_totalNum.text = "\(viewModel.correctNum + viewModel.errorNum)次" |
| | | label_coin.text = "恭喜您,已完成全部答题!获得0积分!" |
| | | label_correctNum.text = "\(viewModel.correctNum)次" |
| | | label_errorNum.text = "\(viewModel.errorNum)次" |
| | | label_totalNum.text = "\(viewModel.correctNum + viewModel.errorNum)次" |
| | | |
| | | if viewModel.correctNum + viewModel.errorNum == 0{ |
| | | label_ratioNum.text = String(format: "正确率:0%%") |
| | | }else{ |
| | | label_ratioNum.text = String(format: "正确率:%ld%%", floor(Double(viewModel.correctNum) / Double(viewModel.correctNum + viewModel.errorNum) * 100).int) |
| | | } |
| | | if viewModel.correctNum + viewModel.errorNum == 0{ |
| | | label_ratioNum.text = String(format: "正确率:0%%") |
| | | }else{ |
| | | label_ratioNum.text = String(format: "正确率:%ld%%", floor(Double(viewModel.correctNum) / Double(viewModel.correctNum + viewModel.errorNum) * 100).int) |
| | | } |
| | | |
| | | if totalNum != nil && viewModel.listenType.value == .game2{ |
| | | label_title_totalNum.text = "总题目:" |
| | | label_title_correctNum.text = "正确题目:" |
| | | label_title_errorNum.text = "错误题目:" |
| | | label_coin.text = "恭喜您,已完成游戏!获得0积分!" |
| | | if totalNum != nil && viewModel.listenType.value == .game2{ |
| | | label_title_totalNum.text = "总题目:" |
| | | label_title_correctNum.text = "正确题目:" |
| | | label_title_errorNum.text = "错误题目:" |
| | | label_coin.text = "恭喜您,已完成游戏!获得0积分!" |
| | | |
| | | label_totalNum.text = "\(totalNum!)" |
| | | label_correctNum.text = "\(viewModel.correctNum)" |
| | | label_errorNum.text = "\(viewModel.errorNum)" |
| | | } |
| | | label_totalNum.text = "\(totalNum!)" |
| | | label_correctNum.text = "\(viewModel.correctNum)" |
| | | label_errorNum.text = "\(viewModel.errorNum)" |
| | | } |
| | | |
| | | switch viewModel.listenType.value{ |
| | | case .lesson5,.game1,.game2,.story1,.story2:btn_next.isHidden = true |
| | | default:btn_next.isHidden = false |
| | | } |
| | | switch viewModel.listenType.value{ |
| | | case .lesson5,.game1,.game2,.story1,.story2:btn_next.isHidden = true |
| | | default:btn_next.isHidden = false |
| | | } |
| | | |
| | | stackView.isHidden = viewModel.listenType.value == .story2 |
| | | label_ratioNum.isHidden = viewModel.listenType.value == .story2 |
| | | stackView.isHidden = viewModel.listenType.value == .story2 |
| | | label_ratioNum.isHidden = viewModel.listenType.value == .story2 |
| | | |
| | | NotificationCenter.default.post(name: MeUserInfoUpdate_Noti, object: nil) |
| | | } |
| | | NotificationCenter.default.post(name: MeUserInfoUpdate_Noti, object: nil) |
| | | } |
| | | |
| | | override func setUI() { |
| | | super.setUI() |
| | | } |
| | | override func setUI() { |
| | | super.setUI() |
| | | } |
| | | |
| | | override func setRx() { |
| | | NotificationCenter.default.rx.notification(StudyCompleteCoinUpdate_Noti).take(until: self.rx.deallocated).subscribe(onNext: {[weak self] data in |
| | | guard let weakSelf = self else { return } |
| | | override func setRx() { |
| | | NotificationCenter.default.rx.notification(StudyCompleteCoinUpdate_Noti).take(until: self.rx.deallocated).subscribe(onNext: {[weak self] data in |
| | | guard let weakSelf = self else { return } |
| | | |
| | | if let coin = data.object as? Int{ |
| | | switch weakSelf.viewModel.listenType.value { |
| | | case .game2:weakSelf.label_coin.text = "恭喜您,已完成游戏!获得\(coin)积分!" |
| | | default:weakSelf.label_coin.text = "恭喜您,已完成全部答题!获得\(coin)积分!" |
| | | } |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | if let coin = data.object as? Int{ |
| | | switch weakSelf.viewModel.listenType.value { |
| | | case .game2:weakSelf.label_coin.text = "恭喜您,已完成游戏!获得\(coin)积分!" |
| | | default:weakSelf.label_coin.text = "恭喜您,已完成全部答题!获得\(coin)积分!" |
| | | } |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | @IBAction func backHomeAction(_ sender: UIButton) { |
| | | backAction() |
| | | } |
| | | @IBAction func backHomeAction(_ sender: UIButton) { |
| | | backAction() |
| | | } |
| | | |
| | | @IBAction func nextAction(_ sender: UIButton) { |
| | | @IBAction func nextAction(_ sender: UIButton) { |
| | | |
| | | |
| | | var toVC:UIViewController? |
| | | for subv in self.navigationController?.viewControllers ?? []{ |
| | | if subv is HomeListenVC{ |
| | | toVC = subv;break |
| | | } |
| | | } |
| | | var toVC:UIViewController? |
| | | for subv in self.navigationController?.viewControllers ?? []{ |
| | | if subv is HomeListenVC{ |
| | | toVC = subv;break |
| | | } |
| | | } |
| | | |
| | | if toVC == nil{ |
| | | self.navigationController?.popToRootViewController(animated: true) |
| | | }else{ |
| | | let nextType = ListenType(rawValue: viewModel.listenType.value.rawValue + 1)! |
| | | if toVC == nil{ |
| | | self.navigationController?.popToRootViewController(animated: true) |
| | | }else{ |
| | | let nextType = ListenType(rawValue: viewModel.listenType.value.rawValue + 1)! |
| | | |
| | | sceneDelegate?.startTimer() |
| | | sceneDelegate?.startTimer() |
| | | |
| | | switch nextType { |
| | | case .lesson2: |
| | | Services.pictureSelectVoice(day:viewModel.day.value!, quarter: viewModel.quarter.value!, week: viewModel.week.value!).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson2,quarter:weakSelf.viewModel.quarter.value!,week: weakSelf.viewModel.week.value!,day:weakSelf.viewModel.day.value!) |
| | | fightVC.title = ListenType.lesson2.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = weakSelf.studyScheduleModel |
| | | weakSelf.push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | switch nextType { |
| | | case .lesson2: |
| | | Services.pictureSelectVoice(day:viewModel.day.value!, quarter: viewModel.quarter.value!, week: viewModel.week.value!).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson2,quarter:weakSelf.viewModel.quarter.value!,week: weakSelf.viewModel.week.value!,day:weakSelf.viewModel.day.value!) |
| | | fightVC.title = ListenType.lesson2.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = weakSelf.studyScheduleModel |
| | | weakSelf.push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | |
| | | |
| | | case .lesson3: |
| | | Services.induceExclude(day: viewModel.day.value!, quarter: viewModel.quarter.value!, week: viewModel.week.value!).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson3,quarter:weakSelf.viewModel.quarter.value!,week: weakSelf.viewModel.week.value!,day:weakSelf.viewModel.day.value!) |
| | | fightVC.title = ListenType.lesson3.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = weakSelf.studyScheduleModel |
| | | weakSelf.push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | case .lesson3: |
| | | Services.induceExclude(day: viewModel.day.value!, quarter: viewModel.quarter.value!, week: viewModel.week.value!).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson3,quarter:weakSelf.viewModel.quarter.value!,week: weakSelf.viewModel.week.value!,day:weakSelf.viewModel.day.value!) |
| | | fightVC.title = ListenType.lesson3.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = weakSelf.studyScheduleModel |
| | | weakSelf.push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | |
| | | case .lesson4: |
| | | Services.questionsAndAnswers(day: viewModel.day.value!, quarter: viewModel.quarter.value!, week: viewModel.week.value!).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson4,quarter:weakSelf.viewModel.quarter.value!,week: weakSelf.viewModel.week.value!,day:weakSelf.viewModel.day.value!) |
| | | fightVC.title = ListenType.lesson4.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = weakSelf.studyScheduleModel |
| | | weakSelf.push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | case .lesson4: |
| | | Services.questionsAndAnswers(day: viewModel.day.value!, quarter: viewModel.quarter.value!, week: viewModel.week.value!).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson4,quarter:weakSelf.viewModel.quarter.value!,week: weakSelf.viewModel.week.value!,day:weakSelf.viewModel.day.value!) |
| | | fightVC.title = ListenType.lesson4.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = weakSelf.studyScheduleModel |
| | | weakSelf.push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | |
| | | case .lesson5: |
| | | Services.pictureMateVoice(day: viewModel.day.value!, quarter: viewModel.quarter.value!, week: viewModel.week.value!).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson5,quarter:weakSelf.viewModel.quarter.value!,week: weakSelf.viewModel.week.value!,day:weakSelf.viewModel.day.value!) |
| | | fightVC.title = ListenType.lesson5.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = weakSelf.studyScheduleModel |
| | | weakSelf.push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | case .lesson5: |
| | | Services.pictureMateVoice(day: viewModel.day.value!, quarter: viewModel.quarter.value!, week: viewModel.week.value!).subscribe(onNext: {[weak self] result in |
| | | guard let weakSelf = self else { return } |
| | | if let data = result.data{ |
| | | let fightVC = HomeListenFightVC(listenType: .lesson5,quarter:weakSelf.viewModel.quarter.value!,week: weakSelf.viewModel.week.value!,day:weakSelf.viewModel.day.value!) |
| | | fightVC.title = ListenType.lesson5.rawTitle |
| | | fightVC.data = data |
| | | fightVC.studyScheduleModel = weakSelf.studyScheduleModel |
| | | weakSelf.push(vc: fightVC) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | |
| | | default:break |
| | | } |
| | | } |
| | | } |
| | | default:break |
| | | } |
| | | } |
| | | } |
| | | |
| | | private func backAction(){ |
| | | for vc in navigationController?.viewControllers ?? []{ |
| | | if vc.isKind(of: HomeListenVC.self){ |
| | | navigationController?.popToViewController(vc, animated: true);break |
| | | } |
| | | } |
| | | } |
| | | private func backAction(){ |
| | | for vc in navigationController?.viewControllers ?? []{ |
| | | if vc.isKind(of: HomeListenVC.self){ |
| | | navigationController?.popToViewController(vc, animated: true);break |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | import RxSwift |
| | | |
| | | class GoodsItemTCell: UITableViewCell { |
| | | |
| | | @IBOutlet weak var label_state: UILabel! |
| | | @IBOutlet weak var label_goodsName: UILabel! |
| | | @IBOutlet weak var label_types: UILabel! |
| | | @IBOutlet weak var label_goodsNum: UILabel! |
| | | @IBOutlet weak var label_receiptInfo: UILabel! |
| | | @IBOutlet weak var label_sendInfo: UILabel! |
| | | @IBOutlet weak var btn_state: UIButton! |
| | | @IBOutlet weak var label_coin: UILabel! |
| | | @IBOutlet weak var img_cover: UIImageView! |
| | | @IBOutlet weak var view_container: UIView! |
| | | |
| | | private var exchangeRecordModel:ExchangeRecordModel? |
| | | private var disposeBag = DisposeBag() |
| | | @IBOutlet weak var label_state: UILabel! |
| | | @IBOutlet weak var label_goodsName: UILabel! |
| | | @IBOutlet weak var label_types: UILabel! |
| | | @IBOutlet weak var label_goodsNum: UILabel! |
| | | @IBOutlet weak var label_receiptInfo: UILabel! |
| | | @IBOutlet weak var label_sendInfo: UILabel! |
| | | @IBOutlet weak var btn_state: UIButton! |
| | | @IBOutlet weak var label_coin: UILabel! |
| | | @IBOutlet weak var img_cover: UIImageView! |
| | | @IBOutlet weak var view_container: UIView! |
| | | |
| | | override func awakeFromNib() { |
| | | private var exchangeRecordModel:ExchangeRecordModel? |
| | | private var disposeBag = DisposeBag() |
| | | |
| | | override func awakeFromNib() { |
| | | super.awakeFromNib() |
| | | selectionStyle = .none |
| | | backgroundColor = .clear |
| | | view_container.jq_addShadows(shadowColor: UIColor(hexStr: "#D9D9D9").withAlphaComponent(0.28), corner: 8, radius: 3, offset: CGSize(width: 0, height: 2), opacity: 1) |
| | | selectionStyle = .none |
| | | backgroundColor = .clear |
| | | view_container.jq_addShadows(shadowColor: UIColor(hexStr: "#D9D9D9").withAlphaComponent(0.28), corner: 8, radius: 3, offset: CGSize(width: 0, height: 2), opacity: 1) |
| | | } |
| | | |
| | | func setModel(_ model:ExchangeRecordModel){ |
| | | exchangeRecordModel = model |
| | | label_goodsNum.text = "商品数量:\(model.count)" |
| | | label_coin.text = "\(model.integral)积分" |
| | | label_goodsName.text = model.goodsName |
| | | label_types.text = model.goodsType.joined(separator: "|") |
| | | img_cover.sd_setImage(with: URL(string: model.coverImg)) |
| | | var items_consignee = Array<String>() |
| | | items_consignee.append(model.consigneeName) |
| | | items_consignee.append(model.consigneePhone) |
| | | items_consignee.append(model.consigneeAddress) |
| | | label_receiptInfo.text = "收货信息:" + items_consignee.joined(separator: "|") |
| | | func setModel(_ model:ExchangeRecordModel){ |
| | | exchangeRecordModel = model |
| | | label_goodsNum.text = "商品数量:\(model.count)" |
| | | label_coin.text = "\(model.integral)积分" |
| | | label_goodsName.text = model.goodsName |
| | | label_types.text = model.goodsType.joined(separator: "|") |
| | | img_cover.sd_setImage(with: URL(string: model.coverImg)) |
| | | var items_consignee = Array<String>() |
| | | items_consignee.append(model.consigneeName) |
| | | items_consignee.append(model.consigneePhone) |
| | | items_consignee.append(model.consigneeAddress) |
| | | label_receiptInfo.text = "收货信息:" + items_consignee.joined(separator: "|") |
| | | |
| | | var items_express = Array<String>() |
| | | items_express.append(model.express) |
| | | items_express.append(model.expressNumber) |
| | | var items_express = Array<String>() |
| | | items_express.append(model.express) |
| | | items_express.append(model.expressNumber) |
| | | |
| | | label_sendInfo.isHidden = items_express.filter({!$0.isEmpty}).count == 0 |
| | | label_sendInfo.text = "发货信息:" + items_express.joined(separator: "|") |
| | | label_sendInfo.isHidden = items_express.filter({!$0.isEmpty}).count == 0 |
| | | label_sendInfo.text = "发货信息:" + items_express.joined(separator: "|") |
| | | |
| | | //订单状态1待发货2已发货3已完成 |
| | | switch model.state{ |
| | | case 1: |
| | | label_state.text = "待发货" |
| | | btn_state.setTitle("修改地址", for: .normal) |
| | | btn_state.isHidden = false |
| | | case 2: |
| | | label_state.text = "平台已发货,请耐心等待" |
| | | btn_state.setTitle("已收货", for: .normal) |
| | | btn_state.isHidden = false |
| | | case 3: |
| | | label_state.text = "已完成" |
| | | btn_state.isHidden = true |
| | | default: |
| | | btn_state.isHidden = true |
| | | } |
| | | } |
| | | //订单状态1待发货2已发货3已完成 |
| | | switch model.state{ |
| | | case 1: |
| | | label_state.text = "待发货" |
| | | btn_state.setTitle("修改地址", for: .normal) |
| | | btn_state.isHidden = false |
| | | case 2: |
| | | label_state.text = "平台已发货,请耐心等待" |
| | | btn_state.setTitle("已收货", for: .normal) |
| | | btn_state.isHidden = false |
| | | case 3: |
| | | label_state.text = "已完成" |
| | | btn_state.isHidden = true |
| | | default: |
| | | btn_state.isHidden = true |
| | | } |
| | | } |
| | | |
| | | @IBAction func handleAction(_ sender: UIButton) { |
| | | @IBAction func handleAction(_ sender: UIButton) { |
| | | |
| | | switch exchangeRecordModel!.state{ |
| | | case 1: |
| | | let vc = AddressManageVC(type: .choose) |
| | | vc.title = "修改地址" |
| | | vc.chooseAddress { m in |
| | | CommonAlertView.show(content: "确认修改当前收货地址吗?") {[weak self] () in |
| | | guard let weakSelf = self else { return } |
| | | if weakSelf.exchangeRecordModel?.consigneeAddress == m.address && weakSelf.exchangeRecordModel?.consigneeName == m.recipient && weakSelf.exchangeRecordModel?.consigneePhone == m.recipientPhone{ |
| | | alertError(msg: "修改地址信息与原地址信息相同");return |
| | | } |
| | | |
| | | Services.updateOrderAddress(orderId: weakSelf.exchangeRecordModel!.orderId, recipientId: m.id).subscribe(onNext: {data in |
| | | alertSuccess(msg: "修改成功") |
| | | DispatchQueue.main.asyncAfter(delay: 1.8) { |
| | | NotificationCenter.default.post(name: Refresh_MarketExchange_Noti, object: nil) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | } |
| | | } |
| | | JQ_currentViewController().jq_push(vc: vc) |
| | | case 2: |
| | | CommonAlertView.show(isSinple: false, content: "确认已收到货吗?") {[weak self] () in |
| | | guard let weakSelf = self else { return } |
| | | Services.confirmStudy(id: weakSelf.exchangeRecordModel!.orderId).subscribe(onNext: {data in |
| | | DispatchQueue.main.asyncAfter(delay: 1.8) { |
| | | NotificationCenter.default.post(name: Refresh_MarketExchange_Noti, object: nil) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | } |
| | | default:break |
| | | } |
| | | } |
| | | switch exchangeRecordModel!.state{ |
| | | case 1: |
| | | let vc = AddressManageVC(type: .choose) |
| | | vc.title = "修改地址" |
| | | vc.chooseAddress { m in |
| | | CommonAlertView.show(content: "确认修改当前收货地址吗?") {[weak self] () in |
| | | guard let weakSelf = self else { return } |
| | | if weakSelf.exchangeRecordModel?.consigneeAddress == m.address && weakSelf.exchangeRecordModel?.consigneeName == m.recipient && weakSelf.exchangeRecordModel?.consigneePhone == m.recipientPhone{ |
| | | alertError(msg: "修改地址信息与原地址信息相同");return |
| | | } |
| | | |
| | | Services.updateOrderAddress(orderId: weakSelf.exchangeRecordModel!.orderId, recipientId: m.id).subscribe(onNext: {data in |
| | | alertSuccess(msg: "修改成功") |
| | | DispatchQueue.main.asyncAfter(delay: 1.8) { |
| | | NotificationCenter.default.post(name: Refresh_MarketExchange_Noti, object: nil) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | } |
| | | } |
| | | JQ_currentViewController().jq_push(vc: vc) |
| | | case 2: |
| | | CommonAlertView.show(isSinple: false, content: "确认已收到货吗?") {[weak self] () in |
| | | guard let weakSelf = self else { return } |
| | | Services.confirmStudy(id: weakSelf.exchangeRecordModel!.orderId).subscribe(onNext: {data in |
| | | DispatchQueue.main.asyncAfter(delay: 1.8) { |
| | | NotificationCenter.default.post(name: Refresh_MarketExchange_Noti, object: nil) |
| | | } |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | } |
| | | default:break |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | import AVFoundation |
| | | |
| | | class StudyHandleView: UIView,JQNibView{ |
| | | @IBOutlet weak var btn_choose: UIButton! |
| | | @IBOutlet weak var view_container: UIView! |
| | | @IBOutlet weak var btn_choose: UIButton! |
| | | @IBOutlet weak var view_choose: UIView! |
| | | @IBOutlet weak var btn_state: UIButton! |
| | | @IBOutlet weak var btn_voice: UIButton! |
| | |
| | | |
| | | var voicePlayer = VoicePlayer.share() |
| | | var vioceSoundUrl:String? |
| | | var isplayend:Bool = false |
| | | |
| | | override func awakeFromNib() { |
| | | super.awakeFromNib() |
| | | btn_state.alpha = 0 |
| | | view_choose.alpha = 0 |
| | | alpha = 0 |
| | | |
| | | btn_voice.setImage(UIImage(named: "icon_play_1"), for: .normal) |
| | | btn_pay.setImage(UIImage(named: "icon_play"), for: .normal) |
| | | view_container.backgroundColor = .white |
| | | |
| | | btn_choose.setImage(UIImage(named: "btn_radio_u"), for: .normal) |
| | | btn_choose.setImage(UIImage(named: "btn_radio"), for: .selected) |
| | | } |
| | | |
| | | func chooseClouse(callback:@escaping (UIButton)->Void){ |
| | |
| | | switch listenType { |
| | | case .lesson1,.lesson5,.game1: |
| | | UIView.animate(withDuration: 0.25) { |
| | | self.btn_state.setImage(UIImage(named: "icon_play"), for: .normal) |
| | | self.btn_state.setImage(UIImage(named: "icon_play"), for: .normal) |
| | | self.btn_voice.alpha = 1 |
| | | self.btn_pay.alpha = 1 |
| | | self.btn_state.alpha = 0 |
| | |
| | | } |
| | | |
| | | func isplaying(){ |
| | | isplayend = true |
| | | alpha = 1 |
| | | switch listenType { |
| | | case .lesson1,.lesson5,.game1: |
| | | UIView.animate(withDuration: 0.25) { |
| | | self.btn_state.setImage(UIImage(named: "icon_playing"), for: .normal) |
| | | self.btn_state.setImage(UIImage(named: "icon_playing")?.themeGreen, for: .normal) |
| | | self.btn_voice.alpha = 0 |
| | | self.btn_pay.alpha = 0 |
| | | self.btn_state.alpha = 1 |
| | |
| | | break |
| | | case .lesson2: |
| | | UIView.animate(withDuration: 0.25) { |
| | | self.btn_state.setImage(UIImage(named: "icon_playing"), for: .normal) |
| | | self.btn_state.setImage(UIImage(named: "icon_playing")?.themeGreen, for: .normal) |
| | | self.btn_voice.alpha = 0 |
| | | self.btn_pay.alpha = 0 |
| | | self.btn_state.alpha = 1 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> |
| | | <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> |
| | | <device id="ipad10_9rounded" orientation="portrait" layout="fullscreen" appearance="light"/> |
| | | <dependencies> |
| | | <deployment identifier="iOS"/> |
| | | <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/> |
| | | <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/> |
| | | <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> |
| | | </dependencies> |
| | | <objects> |
| | |
| | | <rect key="frame" x="0.0" y="0.0" width="298" height="103"/> |
| | | <subviews> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eGw-vD-STe"> |
| | | <rect key="frame" x="91" y="0.0" width="27" height="103"/> |
| | | <rect key="frame" x="89.5" y="0.0" width="28.5" height="103"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_play_1"/> |
| | | </button> |
| | |
| | | <state key="normal" image="icon_success_small"/> |
| | | </button> |
| | | <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mz1-vG-vfG"> |
| | | <rect key="frame" x="174" y="0.0" width="32" height="103"/> |
| | | <rect key="frame" x="174" y="0.0" width="45" height="103"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" image="icon_play"/> |
| | | </button> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xZL-Va-yis"> |
| | | <rect key="frame" x="91" y="0.0" width="115" height="103"/> |
| | | <rect key="frame" x="89.5" y="0.0" width="129.5" height="103"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <connections> |
| | | <action selector="payAction:" destination="iN0-l3-epB" eventType="touchUpInside" id="Hsm-4b-6ud"/> |
| | | </connections> |
| | | </button> |
| | | </subviews> |
| | | <color key="backgroundColor" red="0.25490196078431371" green="0.63529411764705879" blue="0.92156862745098034" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> |
| | | <constraints> |
| | | <constraint firstAttribute="bottom" secondItem="xZL-Va-yis" secondAttribute="bottom" id="6CU-mm-T5g"/> |
| | | <constraint firstAttribute="bottom" secondItem="eGw-vD-STe" secondAttribute="bottom" id="IFe-rp-rkn"/> |
| | |
| | | <outlet property="btn_state" destination="zhb-53-qPO" id="VwF-zX-XaX"/> |
| | | <outlet property="btn_voice" destination="eGw-vD-STe" id="vtB-m3-iOY"/> |
| | | <outlet property="view_choose" destination="xya-RD-K98" id="gz4-Jd-l5U"/> |
| | | <outlet property="view_container" destination="0XQ-2a-X00" id="fpu-P3-hBc"/> |
| | | </connections> |
| | | <point key="canvasLocation" x="89.268292682926827" y="-171.10169491525426"/> |
| | | </view> |
| | |
| | | <resources> |
| | | <image name="btn_radio" width="52" height="52"/> |
| | | <image name="btn_radio_u" width="52" height="52"/> |
| | | <image name="icon_play" width="32" height="32"/> |
| | | <image name="icon_play_1" width="27" height="27"/> |
| | | <image name="icon_play" width="45" height="45"/> |
| | | <image name="icon_play_1" width="28.5" height="30"/> |
| | | <image name="icon_success_small" width="42" height="42"/> |
| | | </resources> |
| | | </document> |
| | |
| | | |
| | | class VoiceHandleView: UIView { |
| | | |
| | | private lazy var img_hint:UIImageView = { |
| | | let img = UIImageView(image: UIImage(named: "icon_play_1")?.themeGreen) |
| | | return img |
| | | }() |
| | | private lazy var img_hint:UIImageView = { |
| | | let img = UIImageView(image: UIImage(named: "icon_play_1")) |
| | | return img |
| | | }() |
| | | |
| | | private lazy var img_hint_playing:UIImageView = { |
| | | private lazy var img_hint_playing:UIImageView = { |
| | | let img = UIImageView(image: UIImage(named: "icon_playing")?.themeGreen) |
| | | img.isHidden = true |
| | | return img |
| | | }() |
| | | img.isHidden = true |
| | | return img |
| | | }() |
| | | |
| | | private lazy var btn_play:UIButton = { |
| | | let btn = UIButton(type: .custom) |
| | | btn.setImage(UIImage(named: "icon_play")?.themeGreen, for: .normal) |
| | | return btn |
| | | }() |
| | | private lazy var btn_play:UIButton = { |
| | | let btn = UIButton(type: .custom) |
| | | btn.setImage(UIImage(named: "icon_play"), for: .normal) |
| | | return btn |
| | | }() |
| | | |
| | | let playBtn = UIButton(type: .custom) |
| | | let playBtn = UIButton(type: .custom) |
| | | |
| | | var playUrl:String? |
| | | var listenType:ListenType? |
| | | private var playAtClouse:((Int)->Void)? |
| | | var isPlayed:Bool = false //是否已播放 |
| | | var playUrl:String? |
| | | var listenType:ListenType? |
| | | private var playAtClouse:((Int)->Void)? |
| | | |
| | | override init(frame: CGRect) { |
| | | super.init(frame: frame) |
| | | setUI() |
| | | override init(frame: CGRect) { |
| | | super.init(frame: frame) |
| | | setUI() |
| | | |
| | | VoicePlayer.share().playEnd { |
| | | self.resetView() |
| | | } |
| | | } |
| | | VoicePlayer.share().playEnd { |
| | | self.resetView() |
| | | } |
| | | } |
| | | |
| | | private func setUI(){ |
| | | // backgroundColor = UIColor(hexString: "#41A2EB") |
| | | private func setUI(){ |
| | | // backgroundColor = UIColor(hexString: "#41A2EB") |
| | | backgroundColor = UIColor.white |
| | | jq_cornerRadius = 8 |
| | | addSubview(img_hint_playing) |
| | | addSubview(img_hint) |
| | | addSubview(btn_play) |
| | | jq_cornerRadius = 8 |
| | | addSubview(img_hint_playing) |
| | | addSubview(img_hint) |
| | | addSubview(btn_play) |
| | | |
| | | img_hint_playing.snp.makeConstraints { make in |
| | | make.center.equalToSuperview() |
| | | make.width.equalTo(45) |
| | | make.height.equalTo(31) |
| | | } |
| | | img_hint_playing.snp.makeConstraints { make in |
| | | make.center.equalToSuperview() |
| | | make.width.equalTo(45) |
| | | make.height.equalTo(31) |
| | | } |
| | | |
| | | btn_play.isUserInteractionEnabled = false |
| | | btn_play.snp.makeConstraints { make in |
| | | make.left.equalTo(img_hint_playing.snp.right).offset(0) |
| | | make.centerY.equalToSuperview() |
| | | make.width.equalTo(32) |
| | | make.height.equalTo(32) |
| | | } |
| | | btn_play.isUserInteractionEnabled = false |
| | | btn_play.snp.makeConstraints { make in |
| | | make.left.equalTo(img_hint_playing.snp.right).offset(0) |
| | | make.centerY.equalToSuperview() |
| | | make.width.equalTo(32) |
| | | make.height.equalTo(32) |
| | | } |
| | | |
| | | img_hint.snp.makeConstraints { make in |
| | | make.right.equalTo(img_hint_playing.snp.left).offset(0) |
| | | make.centerY.equalToSuperview() |
| | | make.width.equalTo(27) |
| | | make.height.equalTo(27) |
| | | } |
| | | img_hint.snp.makeConstraints { make in |
| | | make.right.equalTo(img_hint_playing.snp.left).offset(0) |
| | | make.centerY.equalToSuperview() |
| | | make.width.equalTo(27) |
| | | make.height.equalTo(27) |
| | | } |
| | | |
| | | playBtn.addTarget(self, action: #selector(playingAction), for: .touchUpInside) |
| | | addSubview(playBtn) |
| | | playBtn.snp.makeConstraints { make in |
| | | make.edges.equalToSuperview() |
| | | } |
| | | } |
| | | playBtn.addTarget(self, action: #selector(playingAction), for: .touchUpInside) |
| | | addSubview(playBtn) |
| | | playBtn.snp.makeConstraints { make in |
| | | make.edges.equalToSuperview() |
| | | } |
| | | } |
| | | |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | required init?(coder: NSCoder) { |
| | | fatalError("init(coder:) has not been implemented") |
| | | } |
| | | |
| | | func copyView()->VoiceHandleView{ |
| | | let copyView = VoiceHandleView() |
| | | copyView.listenType = self.listenType |
| | | copyView.playUrl = self.playUrl |
| | | copyView.frame = self.frame |
| | | return copyView |
| | | } |
| | | func copyView()->VoiceHandleView{ |
| | | let copyView = VoiceHandleView() |
| | | copyView.listenType = self.listenType |
| | | copyView.playUrl = self.playUrl |
| | | copyView.frame = self.frame |
| | | return copyView |
| | | } |
| | | |
| | | func resetView(){ |
| | | img_hint.isHidden = false |
| | | btn_play.isHidden = false |
| | | img_hint_playing.isHidden = true |
| | | jq_cornerRadius = 8 |
| | | } |
| | | func resetView(){ |
| | | img_hint.isHidden = false |
| | | btn_play.isHidden = false |
| | | img_hint_playing.isHidden = true |
| | | isPlayed = false |
| | | jq_cornerRadius = 8 |
| | | } |
| | | |
| | | func playing(){ |
| | | img_hint.isHidden = true |
| | | btn_play.isHidden = true |
| | | img_hint_playing.isHidden = false |
| | | } |
| | | func playing(){ |
| | | img_hint.isHidden = true |
| | | btn_play.isHidden = true |
| | | isPlayed = true |
| | | img_hint_playing.isHidden = false |
| | | } |
| | | |
| | | func playAt(_ clouse:@escaping(Int)->Void){ |
| | | self.playAtClouse = clouse |
| | | } |
| | | func playAt(_ clouse:@escaping(Int)->Void){ |
| | | self.playAtClouse = clouse |
| | | } |
| | | |
| | | @objc func playingAction(){ |
| | | if let url = playUrl{ |
| | | playAtClouse?(self.tag) |
| | | VoicePlayer.share().playerAt(url: url) |
| | | playing() |
| | | } |
| | | } |
| | | @objc func playingAction(){ |
| | | if let url = playUrl{ |
| | | playAtClouse?(self.tag) |
| | | VoicePlayer.share().playerAt(url: url) |
| | | playing() |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | |
| | | // 假设这是服务端返回的统一定义的response格式 |
| | | struct BaseResponse<T :HandyJSON>: HandyJSON { |
| | | var sysTime: Int = 0 |
| | | var code: Int = -1 // 服务端返回码 |
| | | var data: T? = nil // 具体的data的格式和业务相关,故用泛型定义 |
| | | var msg: String = "" |
| | | var sysTime: Int = 0 |
| | | var code: Int = -1 // 服务端返回码 |
| | | var data: T? = nil // 具体的data的格式和业务相关,故用泛型定义 |
| | | var msg: String = "" |
| | | } |
| | | |
| | | struct BaseData<T: HandyJSON>: HandyJSON { |
| | | var records = [T]() |
| | | var records = [T]() |
| | | } |
| | | |
| | | struct SimpleModel: HandyJSON { |
| | | |
| | | } |
| | | struct HtmlModel: HandyJSON { |
| | | var content = "" |
| | | var content1 = "" |
| | | var id = 0 |
| | | var type = 0 |
| | | var content = "" |
| | | var content1 = "" |
| | | var id = 0 |
| | | var type = 0 |
| | | } |
| | | |
| | | extension String: HandyJSON{ |
| | |
| | | let SHAKEY = "" |
| | | |
| | | class ParamsAppender: NSObject { |
| | | var url: URL |
| | | var params:Dictionary = [String: Any]() |
| | | var url: URL |
| | | var params:Dictionary = [String: Any]() |
| | | |
| | | private init(url: String){ |
| | | self.url = URL(string: url)! |
| | | } |
| | | private init(url: String){ |
| | | self.url = URL(string: url)! |
| | | } |
| | | |
| | | @discardableResult |
| | | func interface(url: String) -> ParamsAppender { |
| | | self.url.appendPathComponent(url) |
| | | return self |
| | | } |
| | | @discardableResult |
| | | func interface(url: String) -> ParamsAppender { |
| | | self.url.appendPathComponent(url) |
| | | return self |
| | | } |
| | | |
| | | @discardableResult |
| | | func append(key: String,value: Bool) -> ParamsAppender { |
| | | params += ["\(key)":"\(value)"] |
| | | return self |
| | | } |
| | | @discardableResult |
| | | func append(key: String,value: Bool) -> ParamsAppender { |
| | | params += ["\(key)":"\(value)"] |
| | | return self |
| | | } |
| | | |
| | | @discardableResult |
| | | func append(key: String,value: String?) -> ParamsAppender { |
| | | if value != nil && value?.isEmpty == false { |
| | | params += ["\(key)":"\(value!)"] |
| | | } |
| | | return self |
| | | } |
| | | @discardableResult |
| | | func append(key: String,value: String?) -> ParamsAppender { |
| | | if value != nil && value?.isEmpty == false { |
| | | params += ["\(key)":"\(value!)"] |
| | | } |
| | | return self |
| | | } |
| | | |
| | | @discardableResult |
| | | func append(key: String,value: Array<String>) -> ParamsAppender { |
| | | if value.isEmpty == false { |
| | | params += ["\(key)":value] |
| | | } |
| | | return self |
| | | } |
| | | @discardableResult |
| | | func append(key: String,value: Array<String>) -> ParamsAppender { |
| | | if value.isEmpty == false { |
| | | params += ["\(key)":value] |
| | | } |
| | | return self |
| | | } |
| | | |
| | | |
| | | @discardableResult |
| | | func append(key: String, value: Int?) -> ParamsAppender { |
| | | if value != nil{ |
| | | params += ["\(key)":value!] |
| | | } |
| | | return self |
| | | } |
| | | @discardableResult |
| | | func append(key: String, value: Int?) -> ParamsAppender { |
| | | if value != nil{ |
| | | params += ["\(key)":value!] |
| | | } |
| | | return self |
| | | } |
| | | |
| | | @discardableResult |
| | | func append(key: String, value: Int64) -> ParamsAppender { |
| | | params += ["\(key)":value] |
| | | return self |
| | | } |
| | | @discardableResult |
| | | func append(key: String, value: Int64) -> ParamsAppender { |
| | | params += ["\(key)":value] |
| | | return self |
| | | } |
| | | |
| | | @discardableResult |
| | | func append(key: String, value: Double?) -> ParamsAppender { |
| | | if value != nil{ |
| | | params += ["\(key)":value!] |
| | | } |
| | | return self |
| | | } |
| | | @discardableResult |
| | | func append(key: String, value: Double?) -> ParamsAppender { |
| | | if value != nil{ |
| | | params += ["\(key)":value!] |
| | | } |
| | | return self |
| | | } |
| | | |
| | | @discardableResult |
| | | func append(key: String,data: Data?) -> ParamsAppender { |
| | | if data != nil{ |
| | | params += ["\(key)": data!] |
| | | } |
| | | return self |
| | | } |
| | | @discardableResult |
| | | func append(key: String,data: Data?) -> ParamsAppender { |
| | | if data != nil{ |
| | | params += ["\(key)": data!] |
| | | } |
| | | return self |
| | | } |
| | | |
| | | @discardableResult |
| | | func append(key: String,url: URL) -> ParamsAppender { |
| | | params += ["\(key)":"\(url)"] |
| | | return self |
| | | } |
| | | @discardableResult |
| | | func append(key: String,url: URL) -> ParamsAppender { |
| | | params += ["\(key)":"\(url)"] |
| | | return self |
| | | } |
| | | |
| | | @discardableResult |
| | | func append(dic: [String : Any]) -> ParamsAppender { |
| | | params += dic |
| | | return self |
| | | } |
| | | @discardableResult |
| | | func append(dic: [String : Any]) -> ParamsAppender { |
| | | params += dic |
| | | return self |
| | | } |
| | | |
| | | /// 参数加密 |
| | | @discardableResult |
| | | func done() -> Parameters { |
| | | var paramsArray: [String] = [] |
| | | // 排序 |
| | | let sortedArray: [String] = Array(params.keys).sorted() |
| | | /// 参数加密 |
| | | @discardableResult |
| | | func done() -> Parameters { |
| | | var paramsArray: [String] = [] |
| | | // 排序 |
| | | let sortedArray: [String] = Array(params.keys).sorted() |
| | | |
| | | //防止自签名而错误 |
| | | if !sortedArray.contains("sign"){ |
| | | for item in sortedArray{ |
| | | // 拼接字符串 |
| | | if params.has(key: item){ |
| | | paramsArray.append("\(item)=\(params[item]!)") |
| | | } |
| | | } |
| | | let content = paramsArray.joined(separator: "&") |
| | | params += ["sign": "\(content.jq_hmacBase64(algorithm: .SHA1, key: SHAKEY))"] |
| | | //防止自签名而错误 |
| | | if !sortedArray.contains("sign"){ |
| | | for item in sortedArray{ |
| | | // 拼接字符串 |
| | | if params.has(key: item){ |
| | | paramsArray.append("\(item)=\(params[item]!)") |
| | | } |
| | | } |
| | | let content = paramsArray.joined(separator: "&") |
| | | params += ["sign": "\(content.jq_hmacBase64(algorithm: .SHA1, key: SHAKEY))"] |
| | | |
| | | #if DEBUG |
| | | LogInfo("签名:\(content) ----- \(content.jq_hmacBase64(algorithm: .SHA1, key: SHAKEY))") |
| | | LogInfo("签名:\(content) ----- \(content.jq_hmacBase64(algorithm: .SHA1, key: SHAKEY))") |
| | | #endif |
| | | } |
| | | return self.params |
| | | } |
| | | } |
| | | return self.params |
| | | } |
| | | |
| | | class func build(url: String) -> ParamsAppender { |
| | | return ParamsAppender(url: url) |
| | | } |
| | | class func build(url: String) -> ParamsAppender { |
| | | return ParamsAppender(url: url) |
| | | } |
| | | |
| | | } |
| | | class NetworkRequest { |
| | | |
| | | static let sharedSessionManager: Alamofire.Session = { |
| | | let configuration = URLSessionConfiguration.default |
| | | configuration.timeoutIntervalForRequest = 10 |
| | | return Alamofire.Session(configuration: configuration) |
| | | }() |
| | | enum NetRequestError: Error { |
| | | case Other(Int,String) |
| | | case URLNotFound |
| | | case DownloadFailed |
| | | case InvaildSession |
| | | case ModelError(String) |
| | | case DataAnalysis(String) |
| | | } |
| | | static let sharedSessionManager: Alamofire.Session = { |
| | | let configuration = URLSessionConfiguration.default |
| | | configuration.timeoutIntervalForRequest = 10 |
| | | return Alamofire.Session(configuration: configuration) |
| | | }() |
| | | enum NetRequestError: Error { |
| | | case Other(Int,String) |
| | | case URLNotFound |
| | | case DownloadFailed |
| | | case InvaildSession |
| | | case ModelError(String) |
| | | case DataAnalysis(String) |
| | | } |
| | | |
| | | class func request<T: HandyJSON>(params: ParamsAppender, method: HTTPMethod, encoding: ParameterEncoding? = nil, progress: Bool = true,ignoreAlert:Bool = false) -> Observable<BaseResponse<T>>{ |
| | | class func request<T: HandyJSON>(params: ParamsAppender, method: HTTPMethod, encoding: ParameterEncoding? = nil, progress: Bool = true,ignoreAlert:Bool = false) -> Observable<BaseResponse<T>>{ |
| | | |
| | | return Observable<BaseResponse<T>>.create{ ob in |
| | | guard NetworkReachabilityManager.init(host: All_Url)!.isReachable else { |
| | | alertError(msg: "当前网络不可用") |
| | | ob.onError(AFError.invalidURL(url: params.url)) |
| | | return Disposables.create{} |
| | | } |
| | | return Observable<BaseResponse<T>>.create{ ob in |
| | | guard NetworkReachabilityManager.init(host: All_Url)!.isReachable else { |
| | | alertError(msg: "当前网络不可用") |
| | | ob.onError(AFError.invalidURL(url: params.url)) |
| | | return Disposables.create{} |
| | | } |
| | | |
| | | if progress {showHUD()} |
| | | if progress {showHUD()} |
| | | |
| | | var headers = HTTPHeaders() |
| | | var headers = HTTPHeaders() |
| | | |
| | | if let token = LoginTokenModel.getToken()?.access_token{ |
| | | headers.add(name: "Authorization", value: "Bearer" + " " + token) |
| | | LogInfo("USER_token:Bearer \(token)") |
| | | } |
| | | if let token = LoginTokenModel.getToken()?.access_token{ |
| | | headers.add(name: "Authorization", value: "Bearer" + " " + token) |
| | | LogInfo("USER_token:Bearer \(token)") |
| | | } |
| | | |
| | | if encoding is JSONEncoding { |
| | | headers.add(name: "Content-Type", value: "application/json;charset=UTF-8") |
| | | } |
| | | if encoding is JSONEncoding { |
| | | headers.add(name: "Content-Type", value: "application/json;charset=UTF-8") |
| | | } |
| | | |
| | | var newEncoding: ParameterEncoding |
| | | if encoding != nil { |
| | | newEncoding = encoding! |
| | | } else { |
| | | newEncoding = method == .post ? URLEncoding.httpBody : URLEncoding.queryString |
| | | } |
| | | var newEncoding: ParameterEncoding |
| | | if encoding != nil { |
| | | newEncoding = encoding! |
| | | } else { |
| | | newEncoding = method == .post ? URLEncoding.httpBody : URLEncoding.queryString |
| | | } |
| | | |
| | | sharedSessionManager.request(params.url.absoluteString, method: method, parameters:params.done(), encoding: newEncoding, headers:headers).validate().responseData{response in |
| | | LogInfo("请求地址:\(params.url)") |
| | | LogInfo("请求参数:\(params.params)") |
| | | if progress{hiddenHUD()} |
| | | sharedSessionManager.request(params.url.absoluteString, method: method, parameters:params.done(), encoding: newEncoding, headers:headers).validate().responseData{response in |
| | | LogInfo("请求地址:\(params.url)") |
| | | LogInfo("请求参数:\(params.params)") |
| | | if progress{hiddenHUD()} |
| | | |
| | | guard response.error == nil else { |
| | | LogError("\(response.error!)") |
| | | guard response.error == nil else { |
| | | LogError("\(response.error!)") |
| | | |
| | | var errorString = "" |
| | | errorString.append("服务器故障:\(response.error!.localizedDescription)") |
| | | if let code = response.error?.responseCode{ |
| | | errorString.append("\n【错误码:\(code)】") |
| | | } |
| | | if !ignoreAlert{ |
| | | alert(msg: errorString) |
| | | } |
| | | ob.onError(response.error!) |
| | | return |
| | | } |
| | | if let data = response.data,let jsonString = String(data: data, encoding: String.Encoding.utf8){ |
| | | LogResponse(try! JSONSerialization.jsonObject(with: data)) |
| | | if let next = BaseResponse<T>.deserialize(from: jsonString){ |
| | | switch next.code{ |
| | | case 200:ob.onNext(next) |
| | | case 506: |
| | | ob.onError(NetRequestError.Other(next.code,next.msg)) |
| | | case 502: //登录被冻结 |
| | | CommonAlertView.show(isSinple: true, content: next.msg) |
| | | case 501: |
| | | CommonAlertView.show(content: "以下内容仅限会员查看,请先成为会员!", completeTitle: "成为会员") { |
| | | let vc = VIPCenterVC() |
| | | vc.title = "会员中心" |
| | | JQ_currentNavigationController().pushViewController(vc) |
| | | var errorString = "" |
| | | errorString.append("服务器故障:\(response.error!.localizedDescription)") |
| | | if let code = response.error?.responseCode{ |
| | | errorString.append("\n【错误码:\(code)】") |
| | | } |
| | | if !ignoreAlert{ |
| | | alert(msg: errorString) |
| | | } |
| | | ob.onError(response.error!) |
| | | return |
| | | } |
| | | if let data = response.data,let jsonString = String(data: data, encoding: String.Encoding.utf8){ |
| | | LogResponse(try! JSONSerialization.jsonObject(with: data)) |
| | | if let next = BaseResponse<T>.deserialize(from: jsonString){ |
| | | switch next.code{ |
| | | case 200:ob.onNext(next) |
| | | case 506: |
| | | ob.onError(NetRequestError.Other(next.code,next.msg)) |
| | | case 502: //登录被冻结 |
| | | CommonAlertView.show(isSinple: true, content: next.msg) |
| | | case 501: |
| | | CommonAlertView.show(content: "以下内容仅限会员查看,请先成为会员!", completeTitle: "成为会员") { |
| | | let vc = VIPCenterVC() |
| | | vc.title = "会员中心" |
| | | JQ_currentNavigationController().pushViewController(vc) |
| | | |
| | | } cancelClouse: { |
| | | } cancelClouse: { |
| | | |
| | | } |
| | | case 401,505,600: |
| | | if !ignoreAlert{ |
| | | alertError(msg: "登录失效,请重新登录");ob.onError(NetRequestError.InvaildSession) |
| | | } |
| | | sceneDelegate?.needLogin() |
| | | default: |
| | | //503是手机验证码错误 |
| | | if !ignoreAlert{ |
| | | DispatchQueue.main.async { |
| | | alertError(msg: "\(next.msg)") |
| | | } |
| | | } |
| | | ob.onError(NetRequestError.Other(next.code,next.msg)) |
| | | } |
| | | } |
| | | } |
| | | ob.onCompleted() |
| | | } |
| | | return Disposables.create{} |
| | | } |
| | | } |
| | | } |
| | | case 401,505,600: |
| | | if !ignoreAlert{ |
| | | alertError(msg: "登录失效,请重新登录");ob.onError(NetRequestError.InvaildSession) |
| | | } |
| | | sceneDelegate?.needLogin() |
| | | default: |
| | | //503是手机验证码错误 |
| | | if !ignoreAlert{ |
| | | DispatchQueue.main.async { |
| | | alertError(msg: "\(next.msg)") |
| | | } |
| | | } |
| | | ob.onError(NetRequestError.Other(next.code,next.msg)) |
| | | } |
| | | } |
| | | } |
| | | ob.onCompleted() |
| | | } |
| | | return Disposables.create{} |
| | | } |
| | | } |
| | | } |
| | | extension Dictionary { |
| | | mutating func append(dict: Dictionary) { |
| | | dict.forEach { (key, value) in |
| | | self.updateValue(value, forKey: key) |
| | | } |
| | | } |
| | | mutating func append(dict: Dictionary) { |
| | | dict.forEach { (key, value) in |
| | | self.updateValue(value, forKey: key) |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | func createError(text:String,code:Int)->AFError{ |
| | | return AFError.createURLRequestFailed(error: NSError(domain: text, code: code)) |
| | | return AFError.createURLRequestFailed(error: NSError(domain: text, code: code)) |
| | | } |
| | |
| | | import JQTools |
| | | |
| | | #if DEBUG |
| | | //let All_Url = "http://192.168.110.237:9000" |
| | | //let All_Url = "http://vwpmxwbhv59i.guyubao.com" |
| | | let All_Url = "https://dollearn.com/api" |
| | | #else |
| | | let All_Url = "https://dollearn.com/api" |
| | |
| | | } |
| | | |
| | | extension Services{ |
| | | class func weekList(quarter:Int)->Observable<BaseResponse<[ListenWeekModel]>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/weekList") |
| | | params.append(key: "quarter", value: quarter) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func weekList(quarter:Int)->Observable<BaseResponse<[ListenWeekModel]>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/weekList") |
| | | params.append(key: "quarter", value: quarter) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | |
| | | /// 自主学习1-听音选图 |
| | | class func listenSelectPicture(day:Int,quarter:Int,week:Int)->Observable<BaseResponse<ListenNewModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/listenSelectPicture") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "day", value: day) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | /// 自主学习1-听音选图 |
| | | class func listenSelectPicture(day:Int,quarter:Int,week:Int)->Observable<BaseResponse<ListenNewModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/listenSelectPicture") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "day", value: day) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | |
| | | /// 自主学习2-看图选音 |
| | | class func pictureSelectVoice(day:Int,quarter:Int,week:Int)->Observable<BaseResponse<ListenNewModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/pictureSelectVoice") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "day", value: day) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | /// 自主学习2-看图选音 |
| | | class func pictureSelectVoice(day:Int,quarter:Int,week:Int)->Observable<BaseResponse<ListenNewModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/pictureSelectVoice") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "day", value: day) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | /// 自主学习3-归纳排除 |
| | | class func induceExclude(day:Int,quarter:Int,week:Int)->Observable<BaseResponse<ListenNewModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/induceExclude") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "day", value: day) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | /// 自主学习3-归纳排除 |
| | | class func induceExclude(day:Int,quarter:Int,week:Int)->Observable<BaseResponse<ListenNewModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/induceExclude") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "day", value: day) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | /// 自主学习4-有问有答 |
| | | class func questionsAndAnswers(day:Int,quarter:Int,week:Int)->Observable<BaseResponse<ListenNewModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/questionsAndAnswers") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "day", value: day) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | /// 自主学习4-有问有答 |
| | | class func questionsAndAnswers(day:Int,quarter:Int,week:Int)->Observable<BaseResponse<ListenNewModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/questionsAndAnswers") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "day", value: day) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | /// 自主学习5-音图相配 |
| | | class func pictureMateVoice(day:Int,quarter:Int,week:Int)->Observable<BaseResponse<ListenNewModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/pictureMateVoice") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "day", value: day) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | /// 自主学习5-音图相配 |
| | | class func pictureMateVoice(day:Int,quarter:Int,week:Int)->Observable<BaseResponse<ListenNewModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/pictureMateVoice") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "day", value: day) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | /// 完成学习 |
| | | class func completeLearing(type:Int,studyTime:Int,studyIds:String,quarter:Int,week:Int,day:Int,accracy:Int)->Observable<BaseResponse<Int>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/completeLearning") |
| | | .append(key: "type", value: type) |
| | | .append(key: "studyTime", value: studyTime) |
| | | .append(key: "studyIds", value: studyIds) |
| | | .append(key: "week", value: week) |
| | | .append(key: "day", value: day) |
| | | .append(key: "quarter", value: quarter) |
| | | .append(key: "accuracy", value: accracy) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | /// 完成学习 |
| | | class func completeLearing(type:Int,studyTime:Int,studyIds:String,quarter:Int,week:Int,day:Int,accracy:Int)->Observable<BaseResponse<Int>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/completeLearning") |
| | | .append(key: "type", value: type) |
| | | .append(key: "studyTime", value: studyTime) |
| | | .append(key: "studyIds", value: studyIds) |
| | | .append(key: "week", value: week) |
| | | .append(key: "day", value: day) |
| | | .append(key: "quarter", value: quarter) |
| | | .append(key: "accuracy", value: accracy) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | |
| | | /// 完成游戏 |
| | | class func completeGames(gameId:Int,gameName:String,difficulty:Int,accuracy:Int,useTime:Int)->Observable<BaseResponse<Int>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/gameAchievement") |
| | | .append(key: "gameId", value: gameId) |
| | | .append(key: "accuracy", value: accuracy) |
| | | .append(key: "difficulty", value: difficulty) |
| | | .append(key: "useTime", value: useTime) |
| | | .append(key: "gameName", value: gameName) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | /// 完成游戏 |
| | | class func completeGames(gameId:Int,gameName:String,difficulty:Int,accuracy:Int,useTime:Int)->Observable<BaseResponse<Int>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/gameAchievement") |
| | | .append(key: "gameId", value: gameId) |
| | | .append(key: "accuracy", value: accuracy) |
| | | .append(key: "difficulty", value: difficulty) |
| | | .append(key: "useTime", value: useTime) |
| | | .append(key: "gameName", value: gameName) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | |
| | | class func gameHearing(difficulty:Int,quarter:Int,week:Int)->Observable<BaseResponse<Listen1Model>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/gameHearing") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "difficulty", value: difficulty) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func gameHearing(difficulty:Int,quarter:Int,week:Int)->Observable<BaseResponse<Listen1Model>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/gameHearing") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "difficulty", value: difficulty) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func gameMemory(quarter:Int,week:Int)->Observable<BaseResponse<Listen1Model>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/gameMemory") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func gameMemory(quarter:Int,week:Int)->Observable<BaseResponse<Listen1Model>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/gameMemory") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func frameworkMemory(quarter:Int,week:Int)->Observable<BaseResponse<Listen1Model>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/frameworkMemory") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func frameworkMemory(quarter:Int,week:Int)->Observable<BaseResponse<Listen1Model>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/frameworkMemory") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func lookpictureDbu(quarter:Int,week:Int)->Observable<BaseResponse<Listen1Model>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/lookPictureDbu") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func lookpictureDbu(quarter:Int,week:Int)->Observable<BaseResponse<Listen1Model>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/lookPictureDbu") |
| | | params.append(key: "quarter", value: quarter) |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func completeStory(storyId:Int,accuracy:Int,studyTime:Int,type:Int)->Observable<BaseResponse<Int>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/completeStory") |
| | | params.append(key: "storyId", value: storyId) |
| | | params.append(key: "accuracy", value: accuracy) |
| | | params.append(key: "studyTime", value: studyTime) |
| | | params.append(key: "type", value: type) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func completeStory(storyId:Int,accuracy:Int,studyTime:Int,type:Int)->Observable<BaseResponse<Int>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/completeStory") |
| | | params.append(key: "storyId", value: storyId) |
| | | params.append(key: "accuracy", value: accuracy) |
| | | params.append(key: "studyTime", value: studyTime) |
| | | params.append(key: "type", value: type) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func studySchedule(week:Int)->Observable<BaseResponse<StudyScheduleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/studySchedule") |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func studySchedule(week:Int)->Observable<BaseResponse<StudyScheduleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/studySchedule") |
| | | params.append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func teamSchedule(type:ListenType,week:Int,day:Int)->Observable<BaseResponse<TeamScheduleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/teamSchedule") |
| | | params.append(key: "type", value: type.rawValue) |
| | | params.append(key: "week", value: week) |
| | | params.append(key: "day", value: day) |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | class func teamSchedule(type:ListenType,week:Int,day:Int)->Observable<BaseResponse<TeamScheduleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/teamSchedule") |
| | | params.append(key: "type", value: type.rawValue) |
| | | params.append(key: "week", value: week) |
| | | params.append(key: "day", value: day) |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func parentPage()->Observable<BaseResponse<String>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/studyPage") |
| | | return NetworkRequest.request(params: params, method: .post, progress: false) |
| | | } |
| | | class func parentPage()->Observable<BaseResponse<String>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/studyPage") |
| | | return NetworkRequest.request(params: params, method: .post, progress: false) |
| | | } |
| | | |
| | | class func promptVoice()->Observable<BaseResponse<PromptVoiceModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/promptVoice") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | class func promptVoice()->Observable<BaseResponse<PromptVoiceModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/promptVoice") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | } |
| | | |
| | | // MARK: -- 登录部分 |
| | | extension Services{ |
| | | class func sendPhoneCode(phone:String)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/sendPhoneCode") |
| | | params.append(key: "phone", value: phone) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func sendPhoneCode(phone:String)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/sendPhoneCode") |
| | | params.append(key: "phone", value: phone) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func login(phone:String,code:String)->Observable<BaseResponse<LoginModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/studyLogin") |
| | | params.append(key: "phone", value: phone) |
| | | params.append(key: "phoneCode", value: code) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | class func login(phone:String,code:String)->Observable<BaseResponse<LoginModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/studyLogin") |
| | | params.append(key: "phone", value: phone) |
| | | params.append(key: "phoneCode", value: code) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | } |
| | | |
| | | // MARK: -- 首页 |
| | |
| | | |
| | | // MARK: -- 商品 |
| | | extension Services{ |
| | | class func goodRecommend()->Observable<BaseResponse<[RecommendModel]>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/goodRecommend") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false,ignoreAlert: true) |
| | | } |
| | | class func goodRecommend()->Observable<BaseResponse<[RecommendModel]>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/goodRecommend") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false,ignoreAlert: true) |
| | | } |
| | | |
| | | class func goodsList(keywords:String,page:Int,pageSize:Int = 20,type:[String])->Observable<BaseResponse<BaseResponseList<MarketModel>>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/goodListStudy") |
| | | .append(key: "keywords", value: keywords) |
| | | .append(key: "pageNumber", value: page) |
| | | .append(key: "pageSize", value: pageSize) |
| | | .append(key: "type", value: type) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: false) |
| | | } |
| | | class func goodsList(keywords:String,page:Int,pageSize:Int = 20,type:[String])->Observable<BaseResponse<BaseResponseList<MarketModel>>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/goodListStudy") |
| | | .append(key: "keywords", value: keywords) |
| | | .append(key: "pageNumber", value: page) |
| | | .append(key: "pageSize", value: pageSize) |
| | | .append(key: "type", value: type) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: false) |
| | | } |
| | | |
| | | class func getIntegral()->Observable<BaseResponse<Int>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/getIntegralStudy") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | class func getIntegral()->Observable<BaseResponse<Int>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/getIntegralStudy") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func goodTypeStudy()->Observable<BaseResponse<[MarketTypeModel]>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/goodTypeStudy") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | class func goodTypeStudy()->Observable<BaseResponse<[MarketTypeModel]>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/goodTypeStudy") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func goodsDetail(goodsId:Int)->Observable<BaseResponse<MarketDetailModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/goodDetail") |
| | | .append(key: "goodId", value: goodsId) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func goodsDetail(goodsId:Int)->Observable<BaseResponse<MarketDetailModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/goodDetail") |
| | | .append(key: "goodId", value: goodsId) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func addressList()->Observable<BaseResponse<[AddressModel]>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/shopAddress") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | class func addressList()->Observable<BaseResponse<[AddressModel]>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/shopAddress") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func deleteAddress(id:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/addressDelete") |
| | | .append(key: "id", value: id) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func deleteAddress(id:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/addressDelete") |
| | | .append(key: "id", value: id) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func redeemNow(goodId:Int)->Observable<BaseResponse<MarketDetailModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/redeemNow") |
| | | .append(key: "goodId", value: goodId) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func redeemNow(goodId:Int)->Observable<BaseResponse<MarketDetailModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/redeemNow") |
| | | .append(key: "goodId", value: goodId) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func addressTree()->Observable<BaseResponse<[AddressTreeModel]>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/addressTree") |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func addressTree()->Observable<BaseResponse<[AddressTreeModel]>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/addressTree") |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func addressSaveOrUpdate(id:Int?,province:AddressTreeModel?,city:AddressTreeModel?,country:AddressTreeModel?,userName:String?,userPhone:String?,isDefault:Bool,detailAddress:String)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/addressSaveOrUpdate") |
| | | .append(key: "id", value: id) |
| | | .append(key: "address", value: detailAddress) |
| | | .append(key: "city", value: city?.name) |
| | | .append(key: "cityCode", value: city?.code) |
| | | .append(key: "province", value: province?.name) |
| | | .append(key: "provinceCode", value: province?.code) |
| | | .append(key: "recipient", value: userName) |
| | | .append(key: "recipientPhone", value: userPhone) |
| | | .append(key: "isDefault", value: (isDefault == true) ? 1:0) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | class func addressSaveOrUpdate(id:Int?,province:AddressTreeModel?,city:AddressTreeModel?,country:AddressTreeModel?,userName:String?,userPhone:String?,isDefault:Bool,detailAddress:String)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/addressSaveOrUpdate") |
| | | .append(key: "id", value: id) |
| | | .append(key: "address", value: detailAddress) |
| | | .append(key: "city", value: city?.name) |
| | | .append(key: "cityCode", value: city?.code) |
| | | .append(key: "province", value: province?.name) |
| | | .append(key: "provinceCode", value: province?.code) |
| | | .append(key: "recipient", value: userName) |
| | | .append(key: "recipientPhone", value: userPhone) |
| | | .append(key: "isDefault", value: (isDefault == true) ? 1:0) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | |
| | | class func setDefaultStudy(id:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/setDefaultStudy") |
| | | .append(key: "id", value: id) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func setDefaultStudy(id:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/setDefaultStudy") |
| | | .append(key: "id", value: id) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func goodsExchangeStudy(goodsId:Int,number:Int,orderNumber:String,recipientId:Int,remark:String)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/goodExchangeStudy") |
| | | .append(key: "goodId", value: goodsId) |
| | | .append(key: "number", value: number) |
| | | .append(key: "orderNumber", value: orderNumber) |
| | | .append(key: "recipientId", value: recipientId) |
| | | .append(key: "remark", value: remark) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true,ignoreAlert: true) |
| | | } |
| | | class func goodsExchangeStudy(goodsId:Int,number:Int,orderNumber:String,recipientId:Int,remark:String)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/goodExchangeStudy") |
| | | .append(key: "goodId", value: goodsId) |
| | | .append(key: "number", value: number) |
| | | .append(key: "orderNumber", value: orderNumber) |
| | | .append(key: "recipientId", value: recipientId) |
| | | .append(key: "remark", value: remark) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true,ignoreAlert: true) |
| | | } |
| | | |
| | | class func userInfo()->Observable<BaseResponse<UserInfoModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/userInfo") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | class func userInfo()->Observable<BaseResponse<UserInfoModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/userInfo") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func integralDetail(pageNum:Int,pageSize:Int = 20,time:String?)->Observable<BaseResponse<BaseResponseList<IntegralModel>>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/integralDetail") |
| | | .append(key: "pageNum", value: pageNum) |
| | | .append(key: "pageSize", value: pageSize) |
| | | .append(key: "time", value: time) |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | class func integralDetail(pageNum:Int,pageSize:Int = 20,time:String?)->Observable<BaseResponse<BaseResponseList<IntegralModel>>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/integralDetail") |
| | | .append(key: "pageNum", value: pageNum) |
| | | .append(key: "pageSize", value: pageSize) |
| | | .append(key: "time", value: time) |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func exchangeRecord(page:Int,pageSize:Int = 20)->Observable<BaseResponse<BaseResponseList<ExchangeRecordModel>>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/exchangeRecord") |
| | | .append(key: "pageNumber", value: page) |
| | | .append(key: "pageSize", value: pageSize) |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | class func exchangeRecord(page:Int,pageSize:Int = 20)->Observable<BaseResponse<BaseResponseList<ExchangeRecordModel>>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/exchangeRecord") |
| | | .append(key: "pageNumber", value: page) |
| | | .append(key: "pageSize", value: pageSize) |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func studyGamesRecord()->Observable<BaseResponse<StudyGamesModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/studyRecord") |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func studyGamesRecord()->Observable<BaseResponse<StudyGamesModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/studyRecord") |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func logoutStudy()->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/logoutStudy") |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | class func logoutStudy()->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/logoutStudy") |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | |
| | | class func exitLearning(type:Int,quarter:Int,week:Int,day:Int,teamIds:[String],topicIds:[String],answerNumber:Int,correctNumber:Int,studyTime:Int,schedule:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/exitLearning") |
| | | .append(key: "week", value: week) |
| | | .append(key: "day", value: day) |
| | | .append(key: "teamIds", value: teamIds.joined(separator: ",")) |
| | | .append(key: "topicIds", value: topicIds.joined(separator: ",")) |
| | | .append(key: "type", value: type) |
| | | .append(key: "answerNumber", value: answerNumber) |
| | | .append(key: "correctNumber", value: correctNumber) |
| | | .append(key: "studyTime", value: studyTime) |
| | | .append(key: "schedule", value: schedule) |
| | | .append(key: "quarter", value: quarter) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | class func exitLearning(type:Int,quarter:Int,week:Int,day:Int,teamIds:[String],topicIds:[String],answerNumber:Int,correctNumber:Int,studyTime:Int,schedule:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/exitLearning") |
| | | .append(key: "week", value: week) |
| | | .append(key: "day", value: day) |
| | | .append(key: "teamIds", value: teamIds.joined(separator: ",")) |
| | | .append(key: "topicIds", value: topicIds.joined(separator: ",")) |
| | | .append(key: "type", value: type) |
| | | .append(key: "answerNumber", value: answerNumber) |
| | | .append(key: "correctNumber", value: correctNumber) |
| | | .append(key: "studyTime", value: studyTime) |
| | | .append(key: "schedule", value: schedule) |
| | | .append(key: "quarter", value: quarter) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | |
| | | class func exitGameOrStory(studyTime:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/exitGameOrStory") |
| | | .append(key: "studyTime", value: studyTime) |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | class func exitGameOrStory(studyTime:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/exitGameOrStory") |
| | | .append(key: "studyTime", value: studyTime) |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func updateOrderAddress(orderId:Int,recipientId:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/updateOrderAddress") |
| | | .append(key: "orderId", value: orderId) |
| | | .append(key: "recipientId", value: recipientId) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | class func updateOrderAddress(orderId:Int,recipientId:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/updateOrderAddress") |
| | | .append(key: "orderId", value: orderId) |
| | | .append(key: "recipientId", value: recipientId) |
| | | return NetworkRequest.request(params: params, method: .get, progress: true) |
| | | } |
| | | |
| | | class func onlineDuration()->Observable<BaseResponse<Int>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/onlineDuration") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | class func onlineDuration()->Observable<BaseResponse<Int>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/onlineDuration") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func giveIntegral()->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/giveIntegral") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func confirmStudy(id:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/confirmStudy") |
| | | .append(key: "id", value: id) |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func shareInfo()->Observable<BaseResponse<ShareInfoModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/shareInfo") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func vipInfo()->Observable<BaseResponse<[VIPInfoModel]>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/vipInfoStudy") |
| | | return NetworkRequest.request(params: params, method: .post, progress: false) |
| | | } |
| | | |
| | | class func loginOff()->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/deleteUserStudy") |
| | | return NetworkRequest.request(params: params, method: .post, progress: true) |
| | | } |
| | | class func giveIntegral()->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/giveIntegral") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | |
| | | /// 支付 |
| | | /// - Parameters: |
| | | /// - count: 月份,12 |
| | | /// - price: 价格 |
| | | class func orderStudent(count:Int,price:Double,payType:Int = 3)->Observable<BaseResponse<PaymentInfoModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/orderStudent") |
| | | .append(key: "count", value: count) |
| | | .append(key: "price", value: price) |
| | | .append(key: "payType", value: payType) |
| | | return NetworkRequest.request(params: params, method: .post, progress: true) |
| | | } |
| | | /// - status: 状态1灰色未答题 2绿色正确 3红色错误 |
| | | class func answerQuestion(id:Int,status:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/answerQuestion") |
| | | .append(key: "id", value: id) |
| | | .append(key: "status", value: status) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: false) |
| | | } |
| | | |
| | | class func pay(orderId:Int,transactionIdentifier:String,payType:Int = 3)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/pay") |
| | | .append(key: "orderId", value: orderId) |
| | | .append(key: "transactionIdentifier", value: transactionIdentifier) |
| | | .append(key: "payType", value: payType) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | /// 2.0新增-重新开始 |
| | | /// - Parameters: |
| | | /// - day: 天 |
| | | /// - type: 题目类型(1:听音选图;2:看图选音;3:归纳排除;4:有问有答;5:音图相配) |
| | | /// - week: 周目 |
| | | class func restart(day:Int,type:Int,week:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/study/restart") |
| | | .append(key: "day", value: day) |
| | | .append(key: "type", value: type) |
| | | .append(key: "week", value: week) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: false) |
| | | } |
| | | |
| | | class func queryOrderState(orderId:Int)->Observable<BaseResponse<Bool>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/queryOrderState") |
| | | .append(key: "orderId", value: orderId) |
| | | return NetworkRequest.request(params: params, method: .post, progress: false) |
| | | } |
| | | class func confirmStudy(id:Int)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/goods/base/goods/confirmStudy") |
| | | .append(key: "id", value: id) |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func shareInfo()->Observable<BaseResponse<ShareInfoModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/shareInfo") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | } |
| | | |
| | | class func vipInfo()->Observable<BaseResponse<[VIPInfoModel]>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/vipInfoStudy") |
| | | return NetworkRequest.request(params: params, method: .post, progress: false) |
| | | } |
| | | |
| | | class func loginOff()->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/deleteUserStudy") |
| | | return NetworkRequest.request(params: params, method: .post, progress: true) |
| | | } |
| | | |
| | | |
| | | /// 支付 |
| | | /// - Parameters: |
| | | /// - count: 月份,12 |
| | | /// - price: 价格 |
| | | class func orderStudent(count:Int,price:Double,payType:Int = 3)->Observable<BaseResponse<PaymentInfoModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/orderStudent") |
| | | .append(key: "count", value: count) |
| | | .append(key: "price", value: price) |
| | | .append(key: "payType", value: payType) |
| | | return NetworkRequest.request(params: params, method: .post, progress: true) |
| | | } |
| | | |
| | | class func pay(orderId:Int,transactionIdentifier:String,payType:Int = 3)->Observable<BaseResponse<SimpleModel>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/pay") |
| | | .append(key: "orderId", value: orderId) |
| | | .append(key: "transactionIdentifier", value: transactionIdentifier) |
| | | .append(key: "payType", value: payType) |
| | | return NetworkRequest.request(params: params, method: .post,encoding: JSONEncoding.default, progress: true) |
| | | } |
| | | |
| | | class func queryOrderState(orderId:Int)->Observable<BaseResponse<Bool>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/queryOrderState") |
| | | .append(key: "orderId", value: orderId) |
| | | return NetworkRequest.request(params: params, method: .post, progress: false) |
| | | } |
| | | |
| | | class func getIsOpen()->Observable<BaseResponse<Bool>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | |
| | | } |
| | | |
| | | extension Services{ |
| | | class func getAgreement(type:AgreementType)->Observable<BaseResponse<String>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/getProtocol") |
| | | params.append(key: "type", value: type.rawValue) |
| | | return NetworkRequest.request(params: params, method: .post, progress: true) |
| | | } |
| | | class func getAgreement(type:AgreementType)->Observable<BaseResponse<String>>{ |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/study/base/user/getProtocol") |
| | | params.append(key: "type", value: type.rawValue) |
| | | return NetworkRequest.request(params: params, method: .post, progress: true) |
| | | } |
| | | } |
| | | |
| | | |
| | | extension Services{ |
| | | static func startNetworkMonitor(){ |
| | | let manager = NetworkReachabilityManager(host: All_Url) |
| | | manager?.startListening(onUpdatePerforming: { status in |
| | | switch status { |
| | | case .notReachable:alertError(msg: "当前网络不可用") |
| | | case .reachable(let type): |
| | | switch type{ |
| | | case .ethernetOrWiFi:alert(msg: "当前为Wi-Fi网络") |
| | | case .cellular:alert(msg: "当前为移动网络") |
| | | } |
| | | default:break |
| | | } |
| | | }) |
| | | } |
| | | static func startNetworkMonitor(){ |
| | | let manager = NetworkReachabilityManager(host: All_Url) |
| | | manager?.startListening(onUpdatePerforming: { status in |
| | | switch status { |
| | | case .notReachable:alertError(msg: "当前网络不可用") |
| | | case .reachable(let type): |
| | | switch type{ |
| | | case .ethernetOrWiFi:alert(msg: "当前为Wi-Fi网络") |
| | | case .cellular:alert(msg: "当前为移动网络") |
| | | } |
| | | default:break |
| | | } |
| | | }) |
| | | } |
| | | } |
New file |
| | |
| | | PODS: |
| | | - Alamofire (5.8.1) |
| | | - CryptoSwift (1.8.0) |
| | | - Differentiator (5.0.0) |
| | | - EmptyDataSet-Swift (5.0.0) |
| | | - FFPage (3.0.0) |
| | | - HandyJSON (5.0.2) |
| | | - IQKeyboardManager (6.5.16) |
| | | - IQKeyboardManagerSwift (6.5.16) |
| | | - JQTools (0.1.5): |
| | | - EmptyDataSet-Swift |
| | | - HandyJSON |
| | | - IQKeyboardManager |
| | | - IQKeyboardManagerSwift |
| | | - MJRefresh |
| | | - ObjectMapper |
| | | - QMUIKit |
| | | - RxCocoa |
| | | - RxDataSources |
| | | - RxSwift |
| | | - SDWebImage |
| | | - SnapKit |
| | | - SVProgressHUD |
| | | - TZImagePickerController |
| | | - UserDefaultsStore (~> 1.5.0) |
| | | - VTMagic |
| | | - XCGLogger |
| | | - Lantern (1.1.5) |
| | | - MJRefresh (3.7.6) |
| | | - ObjcExceptionBridging (1.0.1): |
| | | - ObjcExceptionBridging/ObjcExceptionBridging (= 1.0.1) |
| | | - ObjcExceptionBridging/ObjcExceptionBridging (1.0.1) |
| | | - ObjectMapper (4.2.0) |
| | | - QMUIKit (4.7.0): |
| | | - QMUIKit/QMUIComponents (= 4.7.0) |
| | | - QMUIKit/QMUICore (= 4.7.0) |
| | | - QMUIKit/QMUILog (= 4.7.0) |
| | | - QMUIKit/QMUIMainFrame (= 4.7.0) |
| | | - QMUIKit/QMUIResources (= 4.7.0) |
| | | - QMUIKit/QMUIWeakObjectContainer (= 4.7.0) |
| | | - QMUIKit/QMUIComponents (4.7.0): |
| | | - QMUIKit/QMUIComponents/NavigationBarTransition (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIAlertController (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIAnimation (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIAppearance (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIAssetLibrary (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIBadge (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIButton (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUICAAnimationExtension (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUICALayerExtension (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUICellHeightCache (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUICellHeightKeyCache (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUICellSizeKeyCache (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIConsole (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIDialogViewController (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIEmotionInputManager (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIEmotionView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIEmptyView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIFloatLayoutView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIGridView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIImagePickerLibrary (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIImagePreviewView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIKeyboardManager (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUILabel (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUILogManagerViewController (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUILogWithConfigurationSupported (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIMarqueeLabel (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIModalPresentationViewController (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIMoreOperationController (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIMultipleDelegates (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUINavigationButton (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUINavigationTitleView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIOrderedDictionary (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIPieProgressView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIPopupContainerView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIPopupMenuView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIScrollAnimator (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUISearchBar (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUISearchController (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUISegmentedControl (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIStaticTableView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUITableView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUITableViewCell (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUITableViewProtocols (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUITestView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUITextField (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUITextView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUITheme (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUITips (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIToastView (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIToolbarButton (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIWindowSizeMonitor (= 4.7.0) |
| | | - QMUIKit/QMUIComponents/QMUIZoomImageView (= 4.7.0) |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/NavigationBarTransition (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUINavigationTitleView |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIMainFrame |
| | | - QMUIKit/QMUIComponents/QMUIAlertController (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIAppearance |
| | | - QMUIKit/QMUIComponents/QMUIButton |
| | | - QMUIKit/QMUIComponents/QMUIKeyboardManager |
| | | - QMUIKit/QMUIComponents/QMUILabel |
| | | - QMUIKit/QMUIComponents/QMUIModalPresentationViewController |
| | | - QMUIKit/QMUIComponents/QMUITextField |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIAnimation (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIAppearance (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIAssetLibrary (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIBadge (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUILabel |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIButton (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUICAAnimationExtension (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIMultipleDelegates |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUICALayerExtension (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIMultipleDelegates |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUICellHeightCache (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUITableViewProtocols |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUICellHeightKeyCache (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIMultipleDelegates |
| | | - QMUIKit/QMUIComponents/QMUITableViewProtocols |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUICellSizeKeyCache (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIMultipleDelegates |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIConsole (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIButton |
| | | - QMUIKit/QMUIComponents/QMUICAAnimationExtension |
| | | - QMUIKit/QMUIComponents/QMUICellHeightKeyCache |
| | | - QMUIKit/QMUIComponents/QMUIPopupMenuView |
| | | - QMUIKit/QMUIComponents/QMUITableView |
| | | - QMUIKit/QMUIComponents/QMUITableViewCell |
| | | - QMUIKit/QMUIComponents/QMUITextField |
| | | - QMUIKit/QMUIComponents/QMUITextView |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIResources |
| | | - QMUIKit/QMUIComponents/QMUIDialogViewController (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIAppearance |
| | | - QMUIKit/QMUIComponents/QMUIButton |
| | | - QMUIKit/QMUIComponents/QMUILabel |
| | | - QMUIKit/QMUIComponents/QMUIModalPresentationViewController |
| | | - QMUIKit/QMUIComponents/QMUINavigationTitleView |
| | | - QMUIKit/QMUIComponents/QMUITableView |
| | | - QMUIKit/QMUIComponents/QMUITableViewCell |
| | | - QMUIKit/QMUIComponents/QMUITextField |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIMainFrame |
| | | - QMUIKit/QMUIComponents/QMUIEmotionInputManager (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIEmotionView |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIEmotionView (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIButton |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIResources |
| | | - QMUIKit/QMUIComponents/QMUIEmptyView (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIAppearance |
| | | - QMUIKit/QMUIComponents/QMUIButton |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIFloatLayoutView (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIGridView (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIImagePickerLibrary (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIAlertController |
| | | - QMUIKit/QMUIComponents/QMUIAppearance |
| | | - QMUIKit/QMUIComponents/QMUIAssetLibrary |
| | | - QMUIKit/QMUIComponents/QMUIButton |
| | | - QMUIKit/QMUIComponents/QMUIEmptyView |
| | | - QMUIKit/QMUIComponents/QMUIImagePreviewView |
| | | - QMUIKit/QMUIComponents/QMUINavigationButton |
| | | - QMUIKit/QMUIComponents/QMUITableViewCell |
| | | - QMUIKit/QMUIComponents/QMUIZoomImageView |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIMainFrame |
| | | - QMUIKit/QMUIResources |
| | | - QMUIKit/QMUIComponents/QMUIImagePreviewView (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIAppearance |
| | | - QMUIKit/QMUIComponents/QMUIButton |
| | | - QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout |
| | | - QMUIKit/QMUIComponents/QMUIEmptyView |
| | | - QMUIKit/QMUIComponents/QMUIPieProgressView |
| | | - QMUIKit/QMUIComponents/QMUIZoomImageView |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIMainFrame |
| | | - QMUIKit/QMUIComponents/QMUIKeyboardManager (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIAppearance |
| | | - QMUIKit/QMUIComponents/QMUIMultipleDelegates |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUILabel (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUILogManagerViewController (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIPopupMenuView |
| | | - QMUIKit/QMUIComponents/QMUISearchController |
| | | - QMUIKit/QMUIComponents/QMUIStaticTableView |
| | | - QMUIKit/QMUIComponents/QMUITableView |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIMainFrame |
| | | - QMUIKit/QMUIComponents/QMUILogWithConfigurationSupported (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIMarqueeLabel (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIModalPresentationViewController (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIAppearance |
| | | - QMUIKit/QMUIComponents/QMUIKeyboardManager |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIMoreOperationController (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIAppearance |
| | | - QMUIKit/QMUIComponents/QMUIButton |
| | | - QMUIKit/QMUIComponents/QMUIModalPresentationViewController |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIMultipleDelegates (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUINavigationButton (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIMainFrame |
| | | - QMUIKit/QMUIComponents/QMUINavigationTitleView (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIAppearance |
| | | - QMUIKit/QMUIComponents/QMUIButton |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIOrderedDictionary (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIPieProgressView (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIPopupContainerView (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIAppearance |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIMainFrame |
| | | - QMUIKit/QMUIComponents/QMUIPopupMenuView (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIButton |
| | | - QMUIKit/QMUIComponents/QMUIPopupContainerView |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIScrollAnimator (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIMultipleDelegates |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUISearchBar (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUISearchController (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIEmptyView |
| | | - QMUIKit/QMUIComponents/QMUISearchBar |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIMainFrame |
| | | - QMUIKit/QMUIComponents/QMUISegmentedControl (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIStaticTableView (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIMultipleDelegates |
| | | - QMUIKit/QMUIComponents/QMUITableViewCell |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUITableView (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUITableViewProtocols |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUITableViewCell (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIButton |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUITableViewProtocols (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUITestView (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUITextField (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIMultipleDelegates |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUITextView (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUILabel |
| | | - QMUIKit/QMUIComponents/QMUIMultipleDelegates |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUITheme (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIAlertController |
| | | - QMUIKit/QMUIComponents/QMUIBadge |
| | | - QMUIKit/QMUIComponents/QMUIButton |
| | | - QMUIKit/QMUIComponents/QMUIConsole |
| | | - QMUIKit/QMUIComponents/QMUIEmotionView |
| | | - QMUIKit/QMUIComponents/QMUIEmptyView |
| | | - QMUIKit/QMUIComponents/QMUIGridView |
| | | - QMUIKit/QMUIComponents/QMUIImagePickerLibrary |
| | | - QMUIKit/QMUIComponents/QMUIImagePreviewView |
| | | - QMUIKit/QMUIComponents/QMUILabel |
| | | - QMUIKit/QMUIComponents/QMUIModalPresentationViewController |
| | | - QMUIKit/QMUIComponents/QMUIPopupContainerView |
| | | - QMUIKit/QMUIComponents/QMUIPopupMenuView |
| | | - QMUIKit/QMUIComponents/QMUITextField |
| | | - QMUIKit/QMUIComponents/QMUITextView |
| | | - QMUIKit/QMUIComponents/QMUIToastView |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUITips (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIToastView |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIResources |
| | | - QMUIKit/QMUIComponents/QMUIToastView (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIKeyboardManager |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIToolbarButton (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIWindowSizeMonitor (4.7.0): |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIComponents/QMUIZoomImageView (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIAssetLibrary |
| | | - QMUIKit/QMUIComponents/QMUIButton |
| | | - QMUIKit/QMUIComponents/QMUIEmptyView |
| | | - QMUIKit/QMUIComponents/QMUIPieProgressView |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUIResources |
| | | - QMUIKit/QMUICore (4.7.0): |
| | | - QMUIKit/QMUILog |
| | | - QMUIKit/QMUIWeakObjectContainer |
| | | - QMUIKit/QMUILog (4.7.0) |
| | | - QMUIKit/QMUIMainFrame (4.7.0): |
| | | - QMUIKit/QMUIComponents/QMUIEmptyView |
| | | - QMUIKit/QMUIComponents/QMUIKeyboardManager |
| | | - QMUIKit/QMUIComponents/QMUIMultipleDelegates |
| | | - QMUIKit/QMUIComponents/QMUINavigationTitleView |
| | | - QMUIKit/QMUIComponents/QMUITableView |
| | | - QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView |
| | | - QMUIKit/QMUICore |
| | | - QMUIKit/QMUILog |
| | | - QMUIKit/QMUIResources (4.7.0) |
| | | - QMUIKit/QMUIWeakObjectContainer (4.7.0) |
| | | - RxCocoa (6.6.0): |
| | | - RxRelay (= 6.6.0) |
| | | - RxSwift (= 6.6.0) |
| | | - RxDataSources (5.0.0): |
| | | - Differentiator (~> 5.0) |
| | | - RxCocoa (~> 6.0) |
| | | - RxSwift (~> 6.0) |
| | | - RxRelay (6.6.0): |
| | | - RxSwift (= 6.6.0) |
| | | - RxSwift (6.6.0) |
| | | - SDWebImage (5.18.5): |
| | | - SDWebImage/Core (= 5.18.5) |
| | | - SDWebImage/Core (5.18.5) |
| | | - SnapKit (5.6.0) |
| | | - SPPageMenu (3.5.0) |
| | | - SVProgressHUD (2.3.1): |
| | | - SVProgressHUD/Core (= 2.3.1) |
| | | - SVProgressHUD/Core (2.3.1) |
| | | - SwifterSwift (6.0.0): |
| | | - SwifterSwift/AppKit (= 6.0.0) |
| | | - SwifterSwift/Combine (= 6.0.0) |
| | | - SwifterSwift/CoreAnimation (= 6.0.0) |
| | | - SwifterSwift/CoreGraphics (= 6.0.0) |
| | | - SwifterSwift/CoreLocation (= 6.0.0) |
| | | - SwifterSwift/CryptoKit (= 6.0.0) |
| | | - SwifterSwift/Dispatch (= 6.0.0) |
| | | - SwifterSwift/Foundation (= 6.0.0) |
| | | - SwifterSwift/HealthKit (= 6.0.0) |
| | | - SwifterSwift/MapKit (= 6.0.0) |
| | | - SwifterSwift/SceneKit (= 6.0.0) |
| | | - SwifterSwift/SpriteKit (= 6.0.0) |
| | | - SwifterSwift/StoreKit (= 6.0.0) |
| | | - SwifterSwift/SwiftStdlib (= 6.0.0) |
| | | - SwifterSwift/UIKit (= 6.0.0) |
| | | - SwifterSwift/WebKit (= 6.0.0) |
| | | - SwifterSwift/AppKit (6.0.0) |
| | | - SwifterSwift/Combine (6.0.0) |
| | | - SwifterSwift/CoreAnimation (6.0.0) |
| | | - SwifterSwift/CoreGraphics (6.0.0) |
| | | - SwifterSwift/CoreLocation (6.0.0) |
| | | - SwifterSwift/CryptoKit (6.0.0) |
| | | - SwifterSwift/Dispatch (6.0.0) |
| | | - SwifterSwift/Foundation (6.0.0) |
| | | - SwifterSwift/HealthKit (6.0.0) |
| | | - SwifterSwift/MapKit (6.0.0) |
| | | - SwifterSwift/SceneKit (6.0.0) |
| | | - SwifterSwift/SpriteKit (6.0.0) |
| | | - SwifterSwift/StoreKit (6.0.0) |
| | | - SwifterSwift/SwiftStdlib (6.0.0) |
| | | - SwifterSwift/UIKit (6.0.0) |
| | | - SwifterSwift/WebKit (6.0.0) |
| | | - SwiftyStoreKit (0.16.1) |
| | | - TZImagePickerController (3.8.4): |
| | | - TZImagePickerController/Basic (= 3.8.4) |
| | | - TZImagePickerController/Location (= 3.8.4) |
| | | - TZImagePickerController/Basic (3.8.4) |
| | | - TZImagePickerController/Location (3.8.4) |
| | | - UserDefaultsStore (1.5.0) |
| | | - VTMagic (1.2.4): |
| | | - VTMagic/Core (= 1.2.4) |
| | | - VTMagic/Core (1.2.4) |
| | | - WechatOpenSDK-XCFramework (2.0.2) |
| | | - XCGLogger (7.0.1): |
| | | - XCGLogger/Core (= 7.0.1) |
| | | - XCGLogger/Core (7.0.1): |
| | | - ObjcExceptionBridging |
| | | |
| | | DEPENDENCIES: |
| | | - Alamofire |
| | | - CryptoSwift |
| | | - FFPage |
| | | - JQTools (from `/Users/yvkd/MyProject/JQTools`) |
| | | - Lantern |
| | | - SPPageMenu |
| | | - SVProgressHUD |
| | | - SwifterSwift |
| | | - SwiftyStoreKit |
| | | - WechatOpenSDK-XCFramework |
| | | |
| | | SPEC REPOS: |
| | | trunk: |
| | | - Alamofire |
| | | - CryptoSwift |
| | | - Differentiator |
| | | - EmptyDataSet-Swift |
| | | - FFPage |
| | | - HandyJSON |
| | | - IQKeyboardManager |
| | | - IQKeyboardManagerSwift |
| | | - Lantern |
| | | - MJRefresh |
| | | - ObjcExceptionBridging |
| | | - ObjectMapper |
| | | - QMUIKit |
| | | - RxCocoa |
| | | - RxDataSources |
| | | - RxRelay |
| | | - RxSwift |
| | | - SDWebImage |
| | | - SnapKit |
| | | - SPPageMenu |
| | | - SVProgressHUD |
| | | - SwifterSwift |
| | | - SwiftyStoreKit |
| | | - TZImagePickerController |
| | | - UserDefaultsStore |
| | | - VTMagic |
| | | - WechatOpenSDK-XCFramework |
| | | - XCGLogger |
| | | |
| | | EXTERNAL SOURCES: |
| | | JQTools: |
| | | :path: "/Users/yvkd/MyProject/JQTools" |
| | | |
| | | SPEC CHECKSUMS: |
| | | Alamofire: 3ca42e259043ee0dc5c0cdd76c4bc568b8e42af7 |
| | | CryptoSwift: 52aaf3fce7337552863b1d952e408085f0e65030 |
| | | Differentiator: e8497ceab83c1b10ca233716d547b9af21b9344d |
| | | EmptyDataSet-Swift: eb382c0c87a2d9c678077385a595cec52da38171 |
| | | FFPage: 481cc0f2dde0f6be84a2359b6c86272e0024dc8d |
| | | HandyJSON: 9e4e236f5d2dbefad5155a77417bbea438201c03 |
| | | IQKeyboardManager: 024b54d7dcb765c5bc99882cb4d5ea24a8cb7c3c |
| | | IQKeyboardManagerSwift: 12d89768845bb77b55cc092ecc2b1f9370f06b76 |
| | | JQTools: d2b720c901e39d9959c9342ba42f9eba58886a02 |
| | | Lantern: b192e7146c6d04e15e627f37281254a6a8593703 |
| | | MJRefresh: 2fe7fb43a5167ceda20bb7e63f130c04fd1814a5 |
| | | ObjcExceptionBridging: c30e00eb3700467e695faeea30e26e18bd445001 |
| | | ObjectMapper: 1eb41f610210777375fa806bf161dc39fb832b81 |
| | | QMUIKit: 2fc09ba9a31a44a4081916ed4c41467bac798821 |
| | | RxCocoa: 44a80de90e25b739b5aeaae3c8c371a32e3343cc |
| | | RxDataSources: aa47cc1ed6c500fa0dfecac5c979b723542d79cf |
| | | RxRelay: 45eaa5db8ee4fb50e5ebd57deec0159e97fa51e6 |
| | | RxSwift: a4b44f7d24599f674deebd1818eab82e58410632 |
| | | SDWebImage: 7ac2b7ddc5e8484c79aa90fc4e30b149d6a2c88f |
| | | SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25 |
| | | SPPageMenu: da182aafcec55719d5c326103cc7716c1e48f311 |
| | | SVProgressHUD: 4837c74bdfe2e51e8821c397825996a8d7de6e22 |
| | | SwifterSwift: 66843dc594f8fb890f5543c22545921d44acc004 |
| | | SwiftyStoreKit: 6b9c08810269f030586dac1fae8e75871a82e84a |
| | | TZImagePickerController: f1c9f1cae6ac0e30b31aaa9698f9bf4a7cf5b84f |
| | | UserDefaultsStore: 905e30372ff432197d199ce1f6fe51be7bf69628 |
| | | VTMagic: b49e5f456dbcbfd9a3588ba92417233a105bc193 |
| | | WechatOpenSDK-XCFramework: acdeeda129efbef9532bca8a10c24e1b4b8c7d69 |
| | | XCGLogger: 1943831ef907df55108b0b18657953f868de973b |
| | | |
| | | PODFILE CHECKSUM: d08291ea186e10b071f475d96892da9674a4fc68 |
| | | |
| | | COCOAPODS: 1.15.2 |
New file |
| | |
| | | Copyright (c) 2014-2022 Alamofire Software Foundation (http://alamofire.org/) |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in |
| | | all copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | THE SOFTWARE. |
New file |
| | |
| | |  |
| | | |
| | | [](https://img.shields.io/badge/Swift-5.6_5.7_5.8_5.9-Orange?style=flat-square) |
| | | [](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_vision_OS_Linux_Windows_Android-Green?style=flat-square) |
| | | [](https://img.shields.io/cocoapods/v/Alamofire.svg) |
| | | [](https://github.com/Carthage/Carthage) |
| | | [](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square) |
| | | [](https://forums.swift.org/c/related-projects/alamofire/37) |
| | | |
| | | Alamofire is an HTTP networking library written in Swift. |
| | | |
| | | - [Features](#features) |
| | | - [Component Libraries](#component-libraries) |
| | | - [Requirements](#requirements) |
| | | - [Migration Guides](#migration-guides) |
| | | - [Communication](#communication) |
| | | - [Installation](#installation) |
| | | - [Contributing](#contributing) |
| | | - [Usage](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#using-alamofire) |
| | | - [**Introduction -**](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#introduction) [Making Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#making-requests), [Response Handling](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-handling), [Response Validation](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-validation), [Response Caching](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-caching) |
| | | - **HTTP -** [HTTP Methods](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#http-methods), [Parameters and Parameter Encoder](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md##request-parameters-and-parameter-encoders), [HTTP Headers](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#http-headers), [Authentication](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#authentication) |
| | | - **Large Data -** [Downloading Data to a File](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#downloading-data-to-a-file), [Uploading Data to a Server](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#uploading-data-to-a-server) |
| | | - **Tools -** [Statistical Metrics](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#statistical-metrics), [cURL Command Output](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#curl-command-output) |
| | | - [Advanced Usage](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md) |
| | | - **URL Session -** [Session Manager](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#session), [Session Delegate](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#sessiondelegate), [Request](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#request) |
| | | - **Routing -** [Routing Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#routing-requests), [Adapting and Retrying Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#adapting-and-retrying-requests-with-requestinterceptor) |
| | | - **Model Objects -** [Custom Response Handlers](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#customizing-response-handlers) |
| | | - **Advanced Concurrency -** [Swift Concurrency](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#using-alamofire-with-swift-concurrency) and [Combine](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#using-alamofire-with-combine) |
| | | - **Connection -** [Security](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#security), [Network Reachability](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#network-reachability) |
| | | - [Open Radars](#open-radars) |
| | | - [FAQ](#faq) |
| | | - [Credits](#credits) |
| | | - [Donations](#donations) |
| | | - [License](#license) |
| | | |
| | | ## Features |
| | | |
| | | - [x] Chainable Request / Response Methods |
| | | - [x] Swift Concurrency Support Back to iOS 13, macOS 10.15, tvOS 13, and watchOS 6. |
| | | - [x] Combine Support |
| | | - [x] URL / JSON Parameter Encoding |
| | | - [x] Upload File / Data / Stream / MultipartFormData |
| | | - [x] Download File using Request or Resume Data |
| | | - [x] Authentication with `URLCredential` |
| | | - [x] HTTP Response Validation |
| | | - [x] Upload and Download Progress Closures with Progress |
| | | - [x] cURL Command Output |
| | | - [x] Dynamically Adapt and Retry Requests |
| | | - [x] TLS Certificate and Public Key Pinning |
| | | - [x] Network Reachability |
| | | - [x] Comprehensive Unit and Integration Test Coverage |
| | | - [x] [Complete Documentation](https://alamofire.github.io/Alamofire) |
| | | |
| | | ## Write Requests Fast! |
| | | |
| | | Alamofire's compact syntax and extensive feature set allow requests with powerful features like automatic retry to be written in just a few lines of code. |
| | | |
| | | ```swift |
| | | // Automatic String to URL conversion, Swift concurrency support, and automatic retry. |
| | | let response = await AF.request("https://httpbin.org/get", interceptor: .retryPolicy) |
| | | // Automatic HTTP Basic Auth. |
| | | .authenticate(username: "user", password: "pass") |
| | | // Caching customization. |
| | | .cacheResponse(using: .cache) |
| | | // Redirect customization. |
| | | .redirect(using: .follow) |
| | | // Validate response code and Content-Type. |
| | | .validate() |
| | | // Produce a cURL command for the request. |
| | | .cURLDescription { description in |
| | | print(description) |
| | | } |
| | | // Automatic Decodable support with background parsing. |
| | | .serializingDecodable(DecodableType.self) |
| | | // Await the full response with metrics and a parsed body. |
| | | .response |
| | | // Detailed response description for easy debugging. |
| | | debugPrint(response) |
| | | ``` |
| | | |
| | | ## Component Libraries |
| | | |
| | | In order to keep Alamofire focused specifically on core networking implementations, additional component libraries have been created by the [Alamofire Software Foundation](https://github.com/Alamofire/Foundation) to bring additional functionality to the Alamofire ecosystem. |
| | | |
| | | - [AlamofireImage](https://github.com/Alamofire/AlamofireImage) - An image library including image response serializers, `UIImage` and `UIImageView` extensions, custom image filters, an auto-purging in-memory cache, and a priority-based image downloading system. |
| | | - [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator) - Controls the visibility of the network activity indicator on iOS using Alamofire. It contains configurable delay timers to help mitigate flicker and can support `URLSession` instances not managed by Alamofire. |
| | | |
| | | ## Requirements |
| | | |
| | | | Platform | Minimum Swift Version | Installation | Status | |
| | | | ---------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------ | |
| | | | iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+ | 5.6 | [CocoaPods](#cocoapods), [Carthage](#carthage), [Swift Package Manager](#swift-package-manager), [Manual](#manually) | Fully Tested | |
| | | | Linux | Latest Only | [Swift Package Manager](#swift-package-manager) | Building But Unsupported | |
| | | | Windows | Latest Only | [Swift Package Manager](#swift-package-manager) | Building But Unsupported | |
| | | | Android | Latest Only | [Swift Package Manager](#swift-package-manager) | Building But Unsupported | |
| | | |
| | | #### Known Issues on Linux and Windows |
| | | |
| | | Alamofire builds on Linux, Windows, and Android but there are missing features and many issues in the underlying `swift-corelibs-foundation` that prevent full functionality and may cause crashes. These include: |
| | | |
| | | - `ServerTrustManager` and associated certificate functionality is unavailable, so there is no certificate pinning and no client certificate support. |
| | | - Various methods of HTTP authentication may crash, including HTTP Basic and HTTP Digest. Crashes may occur if responses contain server challenges. |
| | | - Cache control through `CachedResponseHandler` and associated APIs is unavailable, as the underlying delegate methods aren't called. |
| | | - `URLSessionTaskMetrics` are never gathered. |
| | | |
| | | Due to these issues, Alamofire is unsupported on Linux, Windows, and Android. Please report any crashes to the [Swift bug reporter](https://bugs.swift.org). |
| | | |
| | | ## Migration Guides |
| | | |
| | | - [Alamofire 5.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%205.0%20Migration%20Guide.md) |
| | | - [Alamofire 4.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%204.0%20Migration%20Guide.md) |
| | | - [Alamofire 3.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%203.0%20Migration%20Guide.md) |
| | | - [Alamofire 2.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%202.0%20Migration%20Guide.md) |
| | | |
| | | ## Communication |
| | | |
| | | - If you **need help with making network requests** using Alamofire, use [Stack Overflow](https://stackoverflow.com/questions/tagged/alamofire) and tag `alamofire`. |
| | | - If you need to **find or understand an API**, check [our documentation](http://alamofire.github.io/Alamofire/) or [Apple's documentation for `URLSession`](https://developer.apple.com/documentation/foundation/url_loading_system), on top of which Alamofire is built. |
| | | - If you need **help with an Alamofire feature**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire). |
| | | - If you'd like to **discuss Alamofire best practices**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire). |
| | | - If you'd like to **discuss a feature request**, use [our forum on swift.org](https://forums.swift.org/c/related-projects/alamofire). |
| | | - If you **found a bug**, open an issue here on GitHub and follow the guide. The more detail the better! |
| | | |
| | | ## Installation |
| | | |
| | | ### CocoaPods |
| | | |
| | | [CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate Alamofire into your Xcode project using CocoaPods, specify it in your `Podfile`: |
| | | |
| | | ```ruby |
| | | pod 'Alamofire' |
| | | ``` |
| | | |
| | | ### Carthage |
| | | |
| | | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate Alamofire into your Xcode project using Carthage, specify it in your `Cartfile`: |
| | | |
| | | ```ogdl |
| | | github "Alamofire/Alamofire" |
| | | ``` |
| | | |
| | | ### Swift Package Manager |
| | | |
| | | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. |
| | | |
| | | Once you have your Swift package set up, adding Alamofire as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. |
| | | |
| | | ```swift |
| | | dependencies: [ |
| | | .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.8.1")) |
| | | ] |
| | | ``` |
| | | |
| | | ### Manually |
| | | |
| | | If you prefer not to use any of the aforementioned dependency managers, you can integrate Alamofire into your project manually. |
| | | |
| | | #### Embedded Framework |
| | | |
| | | - Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository: |
| | | |
| | | ```bash |
| | | $ git init |
| | | ``` |
| | | |
| | | - Add Alamofire as a git [submodule](https://git-scm.com/docs/git-submodule) by running the following command: |
| | | |
| | | ```bash |
| | | $ git submodule add https://github.com/Alamofire/Alamofire.git |
| | | ``` |
| | | |
| | | - Open the new `Alamofire` folder, and drag the `Alamofire.xcodeproj` into the Project Navigator of your application's Xcode project. |
| | | |
| | | > It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter. |
| | | |
| | | - Select the `Alamofire.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target. |
| | | - Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar. |
| | | - In the tab bar at the top of that window, open the "General" panel. |
| | | - Click on the `+` button under the "Embedded Binaries" section. |
| | | - You will see two different `Alamofire.xcodeproj` folders each with two different versions of the `Alamofire.framework` nested inside a `Products` folder. |
| | | |
| | | > It does not matter which `Products` folder you choose from, but it does matter whether you choose the top or bottom `Alamofire.framework`. |
| | | |
| | | - Select the top `Alamofire.framework` for iOS and the bottom one for macOS. |
| | | |
| | | > You can verify which one you selected by inspecting the build log for your project. The build target for `Alamofire` will be listed as `Alamofire iOS`, `Alamofire macOS`, `Alamofire tvOS`, or `Alamofire watchOS`. |
| | | |
| | | - And that's it! |
| | | |
| | | > The `Alamofire.framework` is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device. |
| | | |
| | | ## Contributing |
| | | |
| | | Before contributing to Alamofire, please read the instructions detailed in our [contribution guide](https://github.com/Alamofire/Alamofire/blob/master/CONTRIBUTING.md). |
| | | |
| | | ## Open Radars |
| | | |
| | | The following radars have some effect on the current implementation of Alamofire. |
| | | |
| | | - [`rdar://21349340`](http://www.openradar.me/radar?id=5517037090635776) - Compiler throwing warning due to toll-free bridging issue in the test case |
| | | - `rdar://26870455` - Background URL Session Configurations do not work in the simulator |
| | | - `rdar://26849668` - Some URLProtocol APIs do not properly handle `URLRequest` |
| | | |
| | | ## Resolved Radars |
| | | |
| | | The following radars have been resolved over time after being filed against the Alamofire project. |
| | | |
| | | - [`rdar://26761490`](http://www.openradar.me/radar?id=5010235949318144) - Swift string interpolation causing memory leak with common usage. |
| | | - (Resolved): 9/1/17 in Xcode 9 beta 6. |
| | | - [`rdar://36082113`](http://openradar.appspot.com/radar?id=4942308441063424) - `URLSessionTaskMetrics` failing to link on watchOS 3.0+ |
| | | - (Resolved): Just add `CFNetwork` to your linked frameworks. |
| | | - `FB7624529` - `urlSession(_:task:didFinishCollecting:)` never called on watchOS |
| | | - (Resolved): Metrics now collected on watchOS 7+. |
| | | |
| | | ## FAQ |
| | | |
| | | ### What's the origin of the name Alamofire? |
| | | |
| | | Alamofire is named after the [Alamo Fire flower](https://aggie-horticulture.tamu.edu/wildseed/alamofire.html), a hybrid variant of the Bluebonnet, the official state flower of Texas. |
| | | |
| | | ## Credits |
| | | |
| | | Alamofire is owned and maintained by the [Alamofire Software Foundation](http://alamofire.org). You can follow them on Twitter at [@AlamofireSF](https://twitter.com/AlamofireSF) for project updates and releases. |
| | | |
| | | ### Security Disclosure |
| | | |
| | | If you believe you have identified a security vulnerability with Alamofire, you should report it as soon as possible via email to security@alamofire.org. Please do not post it to a public issue tracker. |
| | | |
| | | ## Sponsorship |
| | | |
| | | The [ASF](https://github.com/Alamofire/Foundation#members) is looking to raise money to officially stay registered as a federal non-profit organization. |
| | | Registering will allow Foundation members to gain some legal protections and also allow us to put donations to use, tax-free. |
| | | Sponsoring the ASF will enable us to: |
| | | |
| | | - Pay our yearly legal fees to keep the non-profit in good status |
| | | - Pay for our mail servers to help us stay on top of all questions and security issues |
| | | - Potentially fund test servers to make it easier for us to test the edge cases |
| | | - Potentially fund developers to work on one of our projects full-time |
| | | |
| | | The community adoption of the ASF libraries has been amazing. |
| | | We are greatly humbled by your enthusiasm around the projects and want to continue to do everything we can to move the needle forward. |
| | | With your continued support, the ASF will be able to improve its reach and also provide better legal safety for the core members. |
| | | If you use any of our libraries for work, see if your employers would be interested in donating. |
| | | Any amount you can donate, whether once or monthly, to help us reach our goal would be greatly appreciated. |
| | | |
| | | [Sponsor Alamofire](https://github.com/sponsors/Alamofire) |
| | | |
| | | ## Supporters |
| | | |
| | | [MacStadium](https://macstadium.com) provides Alamofire with a free, hosted Mac mini. |
| | | |
| | |  |
| | | |
| | | ## License |
| | | |
| | | Alamofire is released under the MIT license. [See LICENSE](https://github.com/Alamofire/Alamofire/blob/master/LICENSE) for details. |
New file |
| | |
| | | // |
| | | // AFError.swift |
| | | // |
| | | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | #if canImport(Security) |
| | | import Security |
| | | #endif |
| | | |
| | | /// `AFError` is the error type returned by Alamofire. It encompasses a few different types of errors, each with |
| | | /// their own associated reasons. |
| | | public enum AFError: Error { |
| | | /// The underlying reason the `.multipartEncodingFailed` error occurred. |
| | | public enum MultipartEncodingFailureReason { |
| | | /// The `fileURL` provided for reading an encodable body part isn't a file `URL`. |
| | | case bodyPartURLInvalid(url: URL) |
| | | /// The filename of the `fileURL` provided has either an empty `lastPathComponent` or `pathExtension. |
| | | case bodyPartFilenameInvalid(in: URL) |
| | | /// The file at the `fileURL` provided was not reachable. |
| | | case bodyPartFileNotReachable(at: URL) |
| | | /// Attempting to check the reachability of the `fileURL` provided threw an error. |
| | | case bodyPartFileNotReachableWithError(atURL: URL, error: Error) |
| | | /// The file at the `fileURL` provided is actually a directory. |
| | | case bodyPartFileIsDirectory(at: URL) |
| | | /// The size of the file at the `fileURL` provided was not returned by the system. |
| | | case bodyPartFileSizeNotAvailable(at: URL) |
| | | /// The attempt to find the size of the file at the `fileURL` provided threw an error. |
| | | case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error) |
| | | /// An `InputStream` could not be created for the provided `fileURL`. |
| | | case bodyPartInputStreamCreationFailed(for: URL) |
| | | /// An `OutputStream` could not be created when attempting to write the encoded data to disk. |
| | | case outputStreamCreationFailed(for: URL) |
| | | /// The encoded body data could not be written to disk because a file already exists at the provided `fileURL`. |
| | | case outputStreamFileAlreadyExists(at: URL) |
| | | /// The `fileURL` provided for writing the encoded body data to disk is not a file `URL`. |
| | | case outputStreamURLInvalid(url: URL) |
| | | /// The attempt to write the encoded body data to disk failed with an underlying error. |
| | | case outputStreamWriteFailed(error: Error) |
| | | /// The attempt to read an encoded body part `InputStream` failed with underlying system error. |
| | | case inputStreamReadFailed(error: Error) |
| | | } |
| | | |
| | | /// Represents unexpected input stream length that occur when encoding the `MultipartFormData`. Instances will be |
| | | /// embedded within an `AFError.multipartEncodingFailed` `.inputStreamReadFailed` case. |
| | | public struct UnexpectedInputStreamLength: Error { |
| | | /// The expected byte count to read. |
| | | public var bytesExpected: UInt64 |
| | | /// The actual byte count read. |
| | | public var bytesRead: UInt64 |
| | | } |
| | | |
| | | /// The underlying reason the `.parameterEncodingFailed` error occurred. |
| | | public enum ParameterEncodingFailureReason { |
| | | /// The `URLRequest` did not have a `URL` to encode. |
| | | case missingURL |
| | | /// JSON serialization failed with an underlying system error during the encoding process. |
| | | case jsonEncodingFailed(error: Error) |
| | | /// Custom parameter encoding failed due to the associated `Error`. |
| | | case customEncodingFailed(error: Error) |
| | | } |
| | | |
| | | /// The underlying reason the `.parameterEncoderFailed` error occurred. |
| | | public enum ParameterEncoderFailureReason { |
| | | /// Possible missing components. |
| | | public enum RequiredComponent { |
| | | /// The `URL` was missing or unable to be extracted from the passed `URLRequest` or during encoding. |
| | | case url |
| | | /// The `HTTPMethod` could not be extracted from the passed `URLRequest`. |
| | | case httpMethod(rawValue: String) |
| | | } |
| | | |
| | | /// A `RequiredComponent` was missing during encoding. |
| | | case missingRequiredComponent(RequiredComponent) |
| | | /// The underlying encoder failed with the associated error. |
| | | case encoderFailed(error: Error) |
| | | } |
| | | |
| | | /// The underlying reason the `.responseValidationFailed` error occurred. |
| | | public enum ResponseValidationFailureReason { |
| | | /// The data file containing the server response did not exist. |
| | | case dataFileNil |
| | | /// The data file containing the server response at the associated `URL` could not be read. |
| | | case dataFileReadFailed(at: URL) |
| | | /// The response did not contain a `Content-Type` and the `acceptableContentTypes` provided did not contain a |
| | | /// wildcard type. |
| | | case missingContentType(acceptableContentTypes: [String]) |
| | | /// The response `Content-Type` did not match any type in the provided `acceptableContentTypes`. |
| | | case unacceptableContentType(acceptableContentTypes: [String], responseContentType: String) |
| | | /// The response status code was not acceptable. |
| | | case unacceptableStatusCode(code: Int) |
| | | /// Custom response validation failed due to the associated `Error`. |
| | | case customValidationFailed(error: Error) |
| | | } |
| | | |
| | | /// The underlying reason the response serialization error occurred. |
| | | public enum ResponseSerializationFailureReason { |
| | | /// The server response contained no data or the data was zero length. |
| | | case inputDataNilOrZeroLength |
| | | /// The file containing the server response did not exist. |
| | | case inputFileNil |
| | | /// The file containing the server response could not be read from the associated `URL`. |
| | | case inputFileReadFailed(at: URL) |
| | | /// String serialization failed using the provided `String.Encoding`. |
| | | case stringSerializationFailed(encoding: String.Encoding) |
| | | /// JSON serialization failed with an underlying system error. |
| | | case jsonSerializationFailed(error: Error) |
| | | /// A `DataDecoder` failed to decode the response due to the associated `Error`. |
| | | case decodingFailed(error: Error) |
| | | /// A custom response serializer failed due to the associated `Error`. |
| | | case customSerializationFailed(error: Error) |
| | | /// Generic serialization failed for an empty response that wasn't type `Empty` but instead the associated type. |
| | | case invalidEmptyResponse(type: String) |
| | | } |
| | | |
| | | #if canImport(Security) |
| | | /// Underlying reason a server trust evaluation error occurred. |
| | | public enum ServerTrustFailureReason { |
| | | /// The output of a server trust evaluation. |
| | | public struct Output { |
| | | /// The host for which the evaluation was performed. |
| | | public let host: String |
| | | /// The `SecTrust` value which was evaluated. |
| | | public let trust: SecTrust |
| | | /// The `OSStatus` of evaluation operation. |
| | | public let status: OSStatus |
| | | /// The result of the evaluation operation. |
| | | public let result: SecTrustResultType |
| | | |
| | | /// Creates an `Output` value from the provided values. |
| | | init(_ host: String, _ trust: SecTrust, _ status: OSStatus, _ result: SecTrustResultType) { |
| | | self.host = host |
| | | self.trust = trust |
| | | self.status = status |
| | | self.result = result |
| | | } |
| | | } |
| | | |
| | | /// No `ServerTrustEvaluator` was found for the associated host. |
| | | case noRequiredEvaluator(host: String) |
| | | /// No certificates were found with which to perform the trust evaluation. |
| | | case noCertificatesFound |
| | | /// No public keys were found with which to perform the trust evaluation. |
| | | case noPublicKeysFound |
| | | /// During evaluation, application of the associated `SecPolicy` failed. |
| | | case policyApplicationFailed(trust: SecTrust, policy: SecPolicy, status: OSStatus) |
| | | /// During evaluation, setting the associated anchor certificates failed. |
| | | case settingAnchorCertificatesFailed(status: OSStatus, certificates: [SecCertificate]) |
| | | /// During evaluation, creation of the revocation policy failed. |
| | | case revocationPolicyCreationFailed |
| | | /// `SecTrust` evaluation failed with the associated `Error`, if one was produced. |
| | | case trustEvaluationFailed(error: Error?) |
| | | /// Default evaluation failed with the associated `Output`. |
| | | case defaultEvaluationFailed(output: Output) |
| | | /// Host validation failed with the associated `Output`. |
| | | case hostValidationFailed(output: Output) |
| | | /// Revocation check failed with the associated `Output` and options. |
| | | case revocationCheckFailed(output: Output, options: RevocationTrustEvaluator.Options) |
| | | /// Certificate pinning failed. |
| | | case certificatePinningFailed(host: String, trust: SecTrust, pinnedCertificates: [SecCertificate], serverCertificates: [SecCertificate]) |
| | | /// Public key pinning failed. |
| | | case publicKeyPinningFailed(host: String, trust: SecTrust, pinnedKeys: [SecKey], serverKeys: [SecKey]) |
| | | /// Custom server trust evaluation failed due to the associated `Error`. |
| | | case customEvaluationFailed(error: Error) |
| | | } |
| | | #endif |
| | | |
| | | /// The underlying reason the `.urlRequestValidationFailed` |
| | | public enum URLRequestValidationFailureReason { |
| | | /// URLRequest with GET method had body data. |
| | | case bodyDataInGETRequest(Data) |
| | | } |
| | | |
| | | /// `UploadableConvertible` threw an error in `createUploadable()`. |
| | | case createUploadableFailed(error: Error) |
| | | /// `URLRequestConvertible` threw an error in `asURLRequest()`. |
| | | case createURLRequestFailed(error: Error) |
| | | /// `SessionDelegate` threw an error while attempting to move downloaded file to destination URL. |
| | | case downloadedFileMoveFailed(error: Error, source: URL, destination: URL) |
| | | /// `Request` was explicitly cancelled. |
| | | case explicitlyCancelled |
| | | /// `URLConvertible` type failed to create a valid `URL`. |
| | | case invalidURL(url: URLConvertible) |
| | | /// Multipart form encoding failed. |
| | | case multipartEncodingFailed(reason: MultipartEncodingFailureReason) |
| | | /// `ParameterEncoding` threw an error during the encoding process. |
| | | case parameterEncodingFailed(reason: ParameterEncodingFailureReason) |
| | | /// `ParameterEncoder` threw an error while running the encoder. |
| | | case parameterEncoderFailed(reason: ParameterEncoderFailureReason) |
| | | /// `RequestAdapter` threw an error during adaptation. |
| | | case requestAdaptationFailed(error: Error) |
| | | /// `RequestRetrier` threw an error during the request retry process. |
| | | case requestRetryFailed(retryError: Error, originalError: Error) |
| | | /// Response validation failed. |
| | | case responseValidationFailed(reason: ResponseValidationFailureReason) |
| | | /// Response serialization failed. |
| | | case responseSerializationFailed(reason: ResponseSerializationFailureReason) |
| | | #if canImport(Security) |
| | | /// `ServerTrustEvaluating` instance threw an error during trust evaluation. |
| | | case serverTrustEvaluationFailed(reason: ServerTrustFailureReason) |
| | | #endif |
| | | /// `Session` which issued the `Request` was deinitialized, most likely because its reference went out of scope. |
| | | case sessionDeinitialized |
| | | /// `Session` was explicitly invalidated, possibly with the `Error` produced by the underlying `URLSession`. |
| | | case sessionInvalidated(error: Error?) |
| | | /// `URLSessionTask` completed with error. |
| | | case sessionTaskFailed(error: Error) |
| | | /// `URLRequest` failed validation. |
| | | case urlRequestValidationFailed(reason: URLRequestValidationFailureReason) |
| | | } |
| | | |
| | | extension Error { |
| | | /// Returns the instance cast as an `AFError`. |
| | | public var asAFError: AFError? { |
| | | self as? AFError |
| | | } |
| | | |
| | | /// Returns the instance cast as an `AFError`. If casting fails, a `fatalError` with the specified `message` is thrown. |
| | | public func asAFError(orFailWith message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) -> AFError { |
| | | guard let afError = self as? AFError else { |
| | | fatalError(message(), file: file, line: line) |
| | | } |
| | | return afError |
| | | } |
| | | |
| | | /// Casts the instance as `AFError` or returns `defaultAFError` |
| | | func asAFError(or defaultAFError: @autoclosure () -> AFError) -> AFError { |
| | | self as? AFError ?? defaultAFError() |
| | | } |
| | | } |
| | | |
| | | // MARK: - Error Booleans |
| | | |
| | | extension AFError { |
| | | /// Returns whether the instance is `.sessionDeinitialized`. |
| | | public var isSessionDeinitializedError: Bool { |
| | | if case .sessionDeinitialized = self { return true } |
| | | return false |
| | | } |
| | | |
| | | /// Returns whether the instance is `.sessionInvalidated`. |
| | | public var isSessionInvalidatedError: Bool { |
| | | if case .sessionInvalidated = self { return true } |
| | | return false |
| | | } |
| | | |
| | | /// Returns whether the instance is `.explicitlyCancelled`. |
| | | public var isExplicitlyCancelledError: Bool { |
| | | if case .explicitlyCancelled = self { return true } |
| | | return false |
| | | } |
| | | |
| | | /// Returns whether the instance is `.invalidURL`. |
| | | public var isInvalidURLError: Bool { |
| | | if case .invalidURL = self { return true } |
| | | return false |
| | | } |
| | | |
| | | /// Returns whether the instance is `.parameterEncodingFailed`. When `true`, the `underlyingError` property will |
| | | /// contain the associated value. |
| | | public var isParameterEncodingError: Bool { |
| | | if case .parameterEncodingFailed = self { return true } |
| | | return false |
| | | } |
| | | |
| | | /// Returns whether the instance is `.parameterEncoderFailed`. When `true`, the `underlyingError` property will |
| | | /// contain the associated value. |
| | | public var isParameterEncoderError: Bool { |
| | | if case .parameterEncoderFailed = self { return true } |
| | | return false |
| | | } |
| | | |
| | | /// Returns whether the instance is `.multipartEncodingFailed`. When `true`, the `url` and `underlyingError` |
| | | /// properties will contain the associated values. |
| | | public var isMultipartEncodingError: Bool { |
| | | if case .multipartEncodingFailed = self { return true } |
| | | return false |
| | | } |
| | | |
| | | /// Returns whether the instance is `.requestAdaptationFailed`. When `true`, the `underlyingError` property will |
| | | /// contain the associated value. |
| | | public var isRequestAdaptationError: Bool { |
| | | if case .requestAdaptationFailed = self { return true } |
| | | return false |
| | | } |
| | | |
| | | /// Returns whether the instance is `.responseValidationFailed`. When `true`, the `acceptableContentTypes`, |
| | | /// `responseContentType`, `responseCode`, and `underlyingError` properties will contain the associated values. |
| | | public var isResponseValidationError: Bool { |
| | | if case .responseValidationFailed = self { return true } |
| | | return false |
| | | } |
| | | |
| | | /// Returns whether the instance is `.responseSerializationFailed`. When `true`, the `failedStringEncoding` and |
| | | /// `underlyingError` properties will contain the associated values. |
| | | public var isResponseSerializationError: Bool { |
| | | if case .responseSerializationFailed = self { return true } |
| | | return false |
| | | } |
| | | |
| | | #if canImport(Security) |
| | | /// Returns whether the instance is `.serverTrustEvaluationFailed`. When `true`, the `underlyingError` property will |
| | | /// contain the associated value. |
| | | public var isServerTrustEvaluationError: Bool { |
| | | if case .serverTrustEvaluationFailed = self { return true } |
| | | return false |
| | | } |
| | | #endif |
| | | |
| | | /// Returns whether the instance is `requestRetryFailed`. When `true`, the `underlyingError` property will |
| | | /// contain the associated value. |
| | | public var isRequestRetryError: Bool { |
| | | if case .requestRetryFailed = self { return true } |
| | | return false |
| | | } |
| | | |
| | | /// Returns whether the instance is `createUploadableFailed`. When `true`, the `underlyingError` property will |
| | | /// contain the associated value. |
| | | public var isCreateUploadableError: Bool { |
| | | if case .createUploadableFailed = self { return true } |
| | | return false |
| | | } |
| | | |
| | | /// Returns whether the instance is `createURLRequestFailed`. When `true`, the `underlyingError` property will |
| | | /// contain the associated value. |
| | | public var isCreateURLRequestError: Bool { |
| | | if case .createURLRequestFailed = self { return true } |
| | | return false |
| | | } |
| | | |
| | | /// Returns whether the instance is `downloadedFileMoveFailed`. When `true`, the `destination` and `underlyingError` properties will |
| | | /// contain the associated values. |
| | | public var isDownloadedFileMoveError: Bool { |
| | | if case .downloadedFileMoveFailed = self { return true } |
| | | return false |
| | | } |
| | | |
| | | /// Returns whether the instance is `createURLRequestFailed`. When `true`, the `underlyingError` property will |
| | | /// contain the associated value. |
| | | public var isSessionTaskError: Bool { |
| | | if case .sessionTaskFailed = self { return true } |
| | | return false |
| | | } |
| | | } |
| | | |
| | | // MARK: - Convenience Properties |
| | | |
| | | extension AFError { |
| | | /// The `URLConvertible` associated with the error. |
| | | public var urlConvertible: URLConvertible? { |
| | | guard case let .invalidURL(url) = self else { return nil } |
| | | return url |
| | | } |
| | | |
| | | /// The `URL` associated with the error. |
| | | public var url: URL? { |
| | | guard case let .multipartEncodingFailed(reason) = self else { return nil } |
| | | return reason.url |
| | | } |
| | | |
| | | /// The underlying `Error` responsible for generating the failure associated with `.sessionInvalidated`, |
| | | /// `.parameterEncodingFailed`, `.parameterEncoderFailed`, `.multipartEncodingFailed`, `.requestAdaptationFailed`, |
| | | /// `.responseSerializationFailed`, `.requestRetryFailed` errors. |
| | | public var underlyingError: Error? { |
| | | switch self { |
| | | case let .multipartEncodingFailed(reason): |
| | | return reason.underlyingError |
| | | case let .parameterEncodingFailed(reason): |
| | | return reason.underlyingError |
| | | case let .parameterEncoderFailed(reason): |
| | | return reason.underlyingError |
| | | case let .requestAdaptationFailed(error): |
| | | return error |
| | | case let .requestRetryFailed(retryError, _): |
| | | return retryError |
| | | case let .responseValidationFailed(reason): |
| | | return reason.underlyingError |
| | | case let .responseSerializationFailed(reason): |
| | | return reason.underlyingError |
| | | #if canImport(Security) |
| | | case let .serverTrustEvaluationFailed(reason): |
| | | return reason.underlyingError |
| | | #endif |
| | | case let .sessionInvalidated(error): |
| | | return error |
| | | case let .createUploadableFailed(error): |
| | | return error |
| | | case let .createURLRequestFailed(error): |
| | | return error |
| | | case let .downloadedFileMoveFailed(error, _, _): |
| | | return error |
| | | case let .sessionTaskFailed(error): |
| | | return error |
| | | case .explicitlyCancelled, |
| | | .invalidURL, |
| | | .sessionDeinitialized, |
| | | .urlRequestValidationFailed: |
| | | return nil |
| | | } |
| | | } |
| | | |
| | | /// The acceptable `Content-Type`s of a `.responseValidationFailed` error. |
| | | public var acceptableContentTypes: [String]? { |
| | | guard case let .responseValidationFailed(reason) = self else { return nil } |
| | | return reason.acceptableContentTypes |
| | | } |
| | | |
| | | /// The response `Content-Type` of a `.responseValidationFailed` error. |
| | | public var responseContentType: String? { |
| | | guard case let .responseValidationFailed(reason) = self else { return nil } |
| | | return reason.responseContentType |
| | | } |
| | | |
| | | /// The response code of a `.responseValidationFailed` error. |
| | | public var responseCode: Int? { |
| | | guard case let .responseValidationFailed(reason) = self else { return nil } |
| | | return reason.responseCode |
| | | } |
| | | |
| | | /// The `String.Encoding` associated with a failed `.stringResponse()` call. |
| | | public var failedStringEncoding: String.Encoding? { |
| | | guard case let .responseSerializationFailed(reason) = self else { return nil } |
| | | return reason.failedStringEncoding |
| | | } |
| | | |
| | | /// The `source` URL of a `.downloadedFileMoveFailed` error. |
| | | public var sourceURL: URL? { |
| | | guard case let .downloadedFileMoveFailed(_, source, _) = self else { return nil } |
| | | return source |
| | | } |
| | | |
| | | /// The `destination` URL of a `.downloadedFileMoveFailed` error. |
| | | public var destinationURL: URL? { |
| | | guard case let .downloadedFileMoveFailed(_, _, destination) = self else { return nil } |
| | | return destination |
| | | } |
| | | |
| | | #if canImport(Security) |
| | | /// The download resume data of any underlying network error. Only produced by `DownloadRequest`s. |
| | | public var downloadResumeData: Data? { |
| | | (underlyingError as? URLError)?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data |
| | | } |
| | | #endif |
| | | } |
| | | |
| | | extension AFError.ParameterEncodingFailureReason { |
| | | var underlyingError: Error? { |
| | | switch self { |
| | | case let .jsonEncodingFailed(error), |
| | | let .customEncodingFailed(error): |
| | | return error |
| | | case .missingURL: |
| | | return nil |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension AFError.ParameterEncoderFailureReason { |
| | | var underlyingError: Error? { |
| | | switch self { |
| | | case let .encoderFailed(error): |
| | | return error |
| | | case .missingRequiredComponent: |
| | | return nil |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension AFError.MultipartEncodingFailureReason { |
| | | var url: URL? { |
| | | switch self { |
| | | case let .bodyPartURLInvalid(url), |
| | | let .bodyPartFilenameInvalid(url), |
| | | let .bodyPartFileNotReachable(url), |
| | | let .bodyPartFileIsDirectory(url), |
| | | let .bodyPartFileSizeNotAvailable(url), |
| | | let .bodyPartInputStreamCreationFailed(url), |
| | | let .outputStreamCreationFailed(url), |
| | | let .outputStreamFileAlreadyExists(url), |
| | | let .outputStreamURLInvalid(url), |
| | | let .bodyPartFileNotReachableWithError(url, _), |
| | | let .bodyPartFileSizeQueryFailedWithError(url, _): |
| | | return url |
| | | case .outputStreamWriteFailed, |
| | | .inputStreamReadFailed: |
| | | return nil |
| | | } |
| | | } |
| | | |
| | | var underlyingError: Error? { |
| | | switch self { |
| | | case let .bodyPartFileNotReachableWithError(_, error), |
| | | let .bodyPartFileSizeQueryFailedWithError(_, error), |
| | | let .outputStreamWriteFailed(error), |
| | | let .inputStreamReadFailed(error): |
| | | return error |
| | | case .bodyPartURLInvalid, |
| | | .bodyPartFilenameInvalid, |
| | | .bodyPartFileNotReachable, |
| | | .bodyPartFileIsDirectory, |
| | | .bodyPartFileSizeNotAvailable, |
| | | .bodyPartInputStreamCreationFailed, |
| | | .outputStreamCreationFailed, |
| | | .outputStreamFileAlreadyExists, |
| | | .outputStreamURLInvalid: |
| | | return nil |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension AFError.ResponseValidationFailureReason { |
| | | var acceptableContentTypes: [String]? { |
| | | switch self { |
| | | case let .missingContentType(types), |
| | | let .unacceptableContentType(types, _): |
| | | return types |
| | | case .dataFileNil, |
| | | .dataFileReadFailed, |
| | | .unacceptableStatusCode, |
| | | .customValidationFailed: |
| | | return nil |
| | | } |
| | | } |
| | | |
| | | var responseContentType: String? { |
| | | switch self { |
| | | case let .unacceptableContentType(_, responseType): |
| | | return responseType |
| | | case .dataFileNil, |
| | | .dataFileReadFailed, |
| | | .missingContentType, |
| | | .unacceptableStatusCode, |
| | | .customValidationFailed: |
| | | return nil |
| | | } |
| | | } |
| | | |
| | | var responseCode: Int? { |
| | | switch self { |
| | | case let .unacceptableStatusCode(code): |
| | | return code |
| | | case .dataFileNil, |
| | | .dataFileReadFailed, |
| | | .missingContentType, |
| | | .unacceptableContentType, |
| | | .customValidationFailed: |
| | | return nil |
| | | } |
| | | } |
| | | |
| | | var underlyingError: Error? { |
| | | switch self { |
| | | case let .customValidationFailed(error): |
| | | return error |
| | | case .dataFileNil, |
| | | .dataFileReadFailed, |
| | | .missingContentType, |
| | | .unacceptableContentType, |
| | | .unacceptableStatusCode: |
| | | return nil |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension AFError.ResponseSerializationFailureReason { |
| | | var failedStringEncoding: String.Encoding? { |
| | | switch self { |
| | | case let .stringSerializationFailed(encoding): |
| | | return encoding |
| | | case .inputDataNilOrZeroLength, |
| | | .inputFileNil, |
| | | .inputFileReadFailed(_), |
| | | .jsonSerializationFailed(_), |
| | | .decodingFailed(_), |
| | | .customSerializationFailed(_), |
| | | .invalidEmptyResponse: |
| | | return nil |
| | | } |
| | | } |
| | | |
| | | var underlyingError: Error? { |
| | | switch self { |
| | | case let .jsonSerializationFailed(error), |
| | | let .decodingFailed(error), |
| | | let .customSerializationFailed(error): |
| | | return error |
| | | case .inputDataNilOrZeroLength, |
| | | .inputFileNil, |
| | | .inputFileReadFailed, |
| | | .stringSerializationFailed, |
| | | .invalidEmptyResponse: |
| | | return nil |
| | | } |
| | | } |
| | | } |
| | | |
| | | #if canImport(Security) |
| | | extension AFError.ServerTrustFailureReason { |
| | | var output: AFError.ServerTrustFailureReason.Output? { |
| | | switch self { |
| | | case let .defaultEvaluationFailed(output), |
| | | let .hostValidationFailed(output), |
| | | let .revocationCheckFailed(output, _): |
| | | return output |
| | | case .noRequiredEvaluator, |
| | | .noCertificatesFound, |
| | | .noPublicKeysFound, |
| | | .policyApplicationFailed, |
| | | .settingAnchorCertificatesFailed, |
| | | .revocationPolicyCreationFailed, |
| | | .trustEvaluationFailed, |
| | | .certificatePinningFailed, |
| | | .publicKeyPinningFailed, |
| | | .customEvaluationFailed: |
| | | return nil |
| | | } |
| | | } |
| | | |
| | | var underlyingError: Error? { |
| | | switch self { |
| | | case let .customEvaluationFailed(error): |
| | | return error |
| | | case let .trustEvaluationFailed(error): |
| | | return error |
| | | case .noRequiredEvaluator, |
| | | .noCertificatesFound, |
| | | .noPublicKeysFound, |
| | | .policyApplicationFailed, |
| | | .settingAnchorCertificatesFailed, |
| | | .revocationPolicyCreationFailed, |
| | | .defaultEvaluationFailed, |
| | | .hostValidationFailed, |
| | | .revocationCheckFailed, |
| | | .certificatePinningFailed, |
| | | .publicKeyPinningFailed: |
| | | return nil |
| | | } |
| | | } |
| | | } |
| | | #endif |
| | | |
| | | // MARK: - Error Descriptions |
| | | |
| | | extension AFError: LocalizedError { |
| | | public var errorDescription: String? { |
| | | switch self { |
| | | case .explicitlyCancelled: |
| | | return "Request explicitly cancelled." |
| | | case let .invalidURL(url): |
| | | return "URL is not valid: \(url)" |
| | | case let .parameterEncodingFailed(reason): |
| | | return reason.localizedDescription |
| | | case let .parameterEncoderFailed(reason): |
| | | return reason.localizedDescription |
| | | case let .multipartEncodingFailed(reason): |
| | | return reason.localizedDescription |
| | | case let .requestAdaptationFailed(error): |
| | | return "Request adaption failed with error: \(error.localizedDescription)" |
| | | case let .responseValidationFailed(reason): |
| | | return reason.localizedDescription |
| | | case let .responseSerializationFailed(reason): |
| | | return reason.localizedDescription |
| | | case let .requestRetryFailed(retryError, originalError): |
| | | return """ |
| | | Request retry failed with retry error: \(retryError.localizedDescription), \ |
| | | original error: \(originalError.localizedDescription) |
| | | """ |
| | | case .sessionDeinitialized: |
| | | return """ |
| | | Session was invalidated without error, so it was likely deinitialized unexpectedly. \ |
| | | Be sure to retain a reference to your Session for the duration of your requests. |
| | | """ |
| | | case let .sessionInvalidated(error): |
| | | return "Session was invalidated with error: \(error?.localizedDescription ?? "No description.")" |
| | | #if canImport(Security) |
| | | case let .serverTrustEvaluationFailed(reason): |
| | | return "Server trust evaluation failed due to reason: \(reason.localizedDescription)" |
| | | #endif |
| | | case let .urlRequestValidationFailed(reason): |
| | | return "URLRequest validation failed due to reason: \(reason.localizedDescription)" |
| | | case let .createUploadableFailed(error): |
| | | return "Uploadable creation failed with error: \(error.localizedDescription)" |
| | | case let .createURLRequestFailed(error): |
| | | return "URLRequest creation failed with error: \(error.localizedDescription)" |
| | | case let .downloadedFileMoveFailed(error, source, destination): |
| | | return "Moving downloaded file from: \(source) to: \(destination) failed with error: \(error.localizedDescription)" |
| | | case let .sessionTaskFailed(error): |
| | | return "URLSessionTask failed with error: \(error.localizedDescription)" |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension AFError.ParameterEncodingFailureReason { |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case .missingURL: |
| | | return "URL request to encode was missing a URL" |
| | | case let .jsonEncodingFailed(error): |
| | | return "JSON could not be encoded because of error:\n\(error.localizedDescription)" |
| | | case let .customEncodingFailed(error): |
| | | return "Custom parameter encoder failed with error: \(error.localizedDescription)" |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension AFError.ParameterEncoderFailureReason { |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case let .missingRequiredComponent(component): |
| | | return "Encoding failed due to a missing request component: \(component)" |
| | | case let .encoderFailed(error): |
| | | return "The underlying encoder failed with the error: \(error)" |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension AFError.MultipartEncodingFailureReason { |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case let .bodyPartURLInvalid(url): |
| | | return "The URL provided is not a file URL: \(url)" |
| | | case let .bodyPartFilenameInvalid(url): |
| | | return "The URL provided does not have a valid filename: \(url)" |
| | | case let .bodyPartFileNotReachable(url): |
| | | return "The URL provided is not reachable: \(url)" |
| | | case let .bodyPartFileNotReachableWithError(url, error): |
| | | return """ |
| | | The system returned an error while checking the provided URL for reachability. |
| | | URL: \(url) |
| | | Error: \(error) |
| | | """ |
| | | case let .bodyPartFileIsDirectory(url): |
| | | return "The URL provided is a directory: \(url)" |
| | | case let .bodyPartFileSizeNotAvailable(url): |
| | | return "Could not fetch the file size from the provided URL: \(url)" |
| | | case let .bodyPartFileSizeQueryFailedWithError(url, error): |
| | | return """ |
| | | The system returned an error while attempting to fetch the file size from the provided URL. |
| | | URL: \(url) |
| | | Error: \(error) |
| | | """ |
| | | case let .bodyPartInputStreamCreationFailed(url): |
| | | return "Failed to create an InputStream for the provided URL: \(url)" |
| | | case let .outputStreamCreationFailed(url): |
| | | return "Failed to create an OutputStream for URL: \(url)" |
| | | case let .outputStreamFileAlreadyExists(url): |
| | | return "A file already exists at the provided URL: \(url)" |
| | | case let .outputStreamURLInvalid(url): |
| | | return "The provided OutputStream URL is invalid: \(url)" |
| | | case let .outputStreamWriteFailed(error): |
| | | return "OutputStream write failed with error: \(error)" |
| | | case let .inputStreamReadFailed(error): |
| | | return "InputStream read failed with error: \(error)" |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension AFError.ResponseSerializationFailureReason { |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case .inputDataNilOrZeroLength: |
| | | return "Response could not be serialized, input data was nil or zero length." |
| | | case .inputFileNil: |
| | | return "Response could not be serialized, input file was nil." |
| | | case let .inputFileReadFailed(url): |
| | | return "Response could not be serialized, input file could not be read: \(url)." |
| | | case let .stringSerializationFailed(encoding): |
| | | return "String could not be serialized with encoding: \(encoding)." |
| | | case let .jsonSerializationFailed(error): |
| | | return "JSON could not be serialized because of error:\n\(error.localizedDescription)" |
| | | case let .invalidEmptyResponse(type): |
| | | return """ |
| | | Empty response could not be serialized to type: \(type). \ |
| | | Use Empty as the expected type for such responses. |
| | | """ |
| | | case let .decodingFailed(error): |
| | | return "Response could not be decoded because of error:\n\(error.localizedDescription)" |
| | | case let .customSerializationFailed(error): |
| | | return "Custom response serializer failed with error:\n\(error.localizedDescription)" |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension AFError.ResponseValidationFailureReason { |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case .dataFileNil: |
| | | return "Response could not be validated, data file was nil." |
| | | case let .dataFileReadFailed(url): |
| | | return "Response could not be validated, data file could not be read: \(url)." |
| | | case let .missingContentType(types): |
| | | return """ |
| | | Response Content-Type was missing and acceptable content types \ |
| | | (\(types.joined(separator: ","))) do not match "*/*". |
| | | """ |
| | | case let .unacceptableContentType(acceptableTypes, responseType): |
| | | return """ |
| | | Response Content-Type "\(responseType)" does not match any acceptable types: \ |
| | | \(acceptableTypes.joined(separator: ",")). |
| | | """ |
| | | case let .unacceptableStatusCode(code): |
| | | return "Response status code was unacceptable: \(code)." |
| | | case let .customValidationFailed(error): |
| | | return "Custom response validation failed with error: \(error.localizedDescription)" |
| | | } |
| | | } |
| | | } |
| | | |
| | | #if canImport(Security) |
| | | extension AFError.ServerTrustFailureReason { |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case let .noRequiredEvaluator(host): |
| | | return "A ServerTrustEvaluating value is required for host \(host) but none was found." |
| | | case .noCertificatesFound: |
| | | return "No certificates were found or provided for evaluation." |
| | | case .noPublicKeysFound: |
| | | return "No public keys were found or provided for evaluation." |
| | | case .policyApplicationFailed: |
| | | return "Attempting to set a SecPolicy failed." |
| | | case .settingAnchorCertificatesFailed: |
| | | return "Attempting to set the provided certificates as anchor certificates failed." |
| | | case .revocationPolicyCreationFailed: |
| | | return "Attempting to create a revocation policy failed." |
| | | case let .trustEvaluationFailed(error): |
| | | return "SecTrust evaluation failed with error: \(error?.localizedDescription ?? "None")" |
| | | case let .defaultEvaluationFailed(output): |
| | | return "Default evaluation failed for host \(output.host)." |
| | | case let .hostValidationFailed(output): |
| | | return "Host validation failed for host \(output.host)." |
| | | case let .revocationCheckFailed(output, _): |
| | | return "Revocation check failed for host \(output.host)." |
| | | case let .certificatePinningFailed(host, _, _, _): |
| | | return "Certificate pinning failed for host \(host)." |
| | | case let .publicKeyPinningFailed(host, _, _, _): |
| | | return "Public key pinning failed for host \(host)." |
| | | case let .customEvaluationFailed(error): |
| | | return "Custom trust evaluation failed with error: \(error.localizedDescription)" |
| | | } |
| | | } |
| | | } |
| | | #endif |
| | | |
| | | extension AFError.URLRequestValidationFailureReason { |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case let .bodyDataInGETRequest(data): |
| | | return """ |
| | | Invalid URLRequest: Requests with GET method cannot have body data: |
| | | \(String(decoding: data, as: UTF8.self)) |
| | | """ |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // Alamofire.swift |
| | | // |
| | | // Copyright (c) 2014-2021 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Dispatch |
| | | import Foundation |
| | | #if canImport(FoundationNetworking) |
| | | @_exported import FoundationNetworking |
| | | #endif |
| | | |
| | | // Enforce minimum Swift version for all platforms and build systems. |
| | | #if swift(<5.5) |
| | | #error("Alamofire doesn't support Swift versions below 5.5.") |
| | | #endif |
| | | |
| | | /// Reference to `Session.default` for quick bootstrapping and examples. |
| | | public let AF = Session.default |
| | | |
| | | /// Current Alamofire version. Necessary since SPM doesn't use dynamic libraries. Plus this will be more accurate. |
| | | let version = "5.8.0" |
New file |
| | |
| | | // |
| | | // AlamofireExtended.swift |
| | | // |
| | | // Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | /// Type that acts as a generic extension point for all `AlamofireExtended` types. |
| | | public struct AlamofireExtension<ExtendedType> { |
| | | /// Stores the type or meta-type of any extended type. |
| | | public private(set) var type: ExtendedType |
| | | |
| | | /// Create an instance from the provided value. |
| | | /// |
| | | /// - Parameter type: Instance being extended. |
| | | public init(_ type: ExtendedType) { |
| | | self.type = type |
| | | } |
| | | } |
| | | |
| | | /// Protocol describing the `af` extension points for Alamofire extended types. |
| | | public protocol AlamofireExtended { |
| | | /// Type being extended. |
| | | associatedtype ExtendedType |
| | | |
| | | /// Static Alamofire extension point. |
| | | static var af: AlamofireExtension<ExtendedType>.Type { get set } |
| | | /// Instance Alamofire extension point. |
| | | var af: AlamofireExtension<ExtendedType> { get set } |
| | | } |
| | | |
| | | extension AlamofireExtended { |
| | | /// Static Alamofire extension point. |
| | | public static var af: AlamofireExtension<Self>.Type { |
| | | get { AlamofireExtension<Self>.self } |
| | | set {} |
| | | } |
| | | |
| | | /// Instance Alamofire extension point. |
| | | public var af: AlamofireExtension<Self> { |
| | | get { AlamofireExtension(self) } |
| | | set {} |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // AuthenticationInterceptor.swift |
| | | // |
| | | // Copyright (c) 2020 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | /// Types adopting the `AuthenticationCredential` protocol can be used to authenticate `URLRequest`s. |
| | | /// |
| | | /// One common example of an `AuthenticationCredential` is an OAuth2 credential containing an access token used to |
| | | /// authenticate all requests on behalf of a user. The access token generally has an expiration window of 60 minutes |
| | | /// which will then require a refresh of the credential using the refresh token to generate a new access token. |
| | | public protocol AuthenticationCredential { |
| | | /// Whether the credential requires a refresh. This property should always return `true` when the credential is |
| | | /// expired. It is also wise to consider returning `true` when the credential will expire in several seconds or |
| | | /// minutes depending on the expiration window of the credential. |
| | | /// |
| | | /// For example, if the credential is valid for 60 minutes, then it would be wise to return `true` when the |
| | | /// credential is only valid for 5 minutes or less. That ensures the credential will not expire as it is passed |
| | | /// around backend services. |
| | | var requiresRefresh: Bool { get } |
| | | } |
| | | |
| | | // MARK: - |
| | | |
| | | /// Types adopting the `Authenticator` protocol can be used to authenticate `URLRequest`s with an |
| | | /// `AuthenticationCredential` as well as refresh the `AuthenticationCredential` when required. |
| | | public protocol Authenticator: AnyObject { |
| | | /// The type of credential associated with the `Authenticator` instance. |
| | | associatedtype Credential: AuthenticationCredential |
| | | |
| | | /// Applies the `Credential` to the `URLRequest`. |
| | | /// |
| | | /// In the case of OAuth2, the access token of the `Credential` would be added to the `URLRequest` as a Bearer |
| | | /// token to the `Authorization` header. |
| | | /// |
| | | /// - Parameters: |
| | | /// - credential: The `Credential`. |
| | | /// - urlRequest: The `URLRequest`. |
| | | func apply(_ credential: Credential, to urlRequest: inout URLRequest) |
| | | |
| | | /// Refreshes the `Credential` and executes the `completion` closure with the `Result` once complete. |
| | | /// |
| | | /// Refresh can be called in one of two ways. It can be called before the `Request` is actually executed due to |
| | | /// a `requiresRefresh` returning `true` during the adapt portion of the `Request` creation process. It can also |
| | | /// be triggered by a failed `Request` where the authentication server denied access due to an expired or |
| | | /// invalidated access token. |
| | | /// |
| | | /// In the case of OAuth2, this method would use the refresh token of the `Credential` to generate a new |
| | | /// `Credential` using the authentication service. Once complete, the `completion` closure should be called with |
| | | /// the new `Credential`, or the error that occurred. |
| | | /// |
| | | /// In general, if the refresh call fails with certain status codes from the authentication server (commonly a 401), |
| | | /// the refresh token in the `Credential` can no longer be used to generate a valid `Credential`. In these cases, |
| | | /// you will need to reauthenticate the user with their username / password. |
| | | /// |
| | | /// Please note, these are just general examples of common use cases. They are not meant to solve your specific |
| | | /// authentication server challenges. Please work with your authentication server team to ensure your |
| | | /// `Authenticator` logic matches their expectations. |
| | | /// |
| | | /// - Parameters: |
| | | /// - credential: The `Credential` to refresh. |
| | | /// - session: The `Session` requiring the refresh. |
| | | /// - completion: The closure to be executed once the refresh is complete. |
| | | func refresh(_ credential: Credential, for session: Session, completion: @escaping (Result<Credential, Error>) -> Void) |
| | | |
| | | /// Determines whether the `URLRequest` failed due to an authentication error based on the `HTTPURLResponse`. |
| | | /// |
| | | /// If the authentication server **CANNOT** invalidate credentials after they are issued, then simply return `false` |
| | | /// for this method. If the authentication server **CAN** invalidate credentials due to security breaches, then you |
| | | /// will need to work with your authentication server team to understand how to identify when this occurs. |
| | | /// |
| | | /// In the case of OAuth2, where an authentication server can invalidate credentials, you will need to inspect the |
| | | /// `HTTPURLResponse` or possibly the `Error` for when this occurs. This is commonly handled by the authentication |
| | | /// server returning a 401 status code and some additional header to indicate an OAuth2 failure occurred. |
| | | /// |
| | | /// It is very important to understand how your authentication server works to be able to implement this correctly. |
| | | /// For example, if your authentication server returns a 401 when an OAuth2 error occurs, and your downstream |
| | | /// service also returns a 401 when you are not authorized to perform that operation, how do you know which layer |
| | | /// of the backend returned you a 401? You do not want to trigger a refresh unless you know your authentication |
| | | /// server is actually the layer rejecting the request. Again, work with your authentication server team to understand |
| | | /// how to identify an OAuth2 401 error vs. a downstream 401 error to avoid endless refresh loops. |
| | | /// |
| | | /// - Parameters: |
| | | /// - urlRequest: The `URLRequest`. |
| | | /// - response: The `HTTPURLResponse`. |
| | | /// - error: The `Error`. |
| | | /// |
| | | /// - Returns: `true` if the `URLRequest` failed due to an authentication error, `false` otherwise. |
| | | func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool |
| | | |
| | | /// Determines whether the `URLRequest` is authenticated with the `Credential`. |
| | | /// |
| | | /// If the authentication server **CANNOT** invalidate credentials after they are issued, then simply return `true` |
| | | /// for this method. If the authentication server **CAN** invalidate credentials due to security breaches, then |
| | | /// read on. |
| | | /// |
| | | /// When an authentication server can invalidate credentials, it means that you may have a non-expired credential |
| | | /// that appears to be valid, but will be rejected by the authentication server when used. Generally when this |
| | | /// happens, a number of requests are all sent when the application is foregrounded, and all of them will be |
| | | /// rejected by the authentication server in the order they are received. The first failed request will trigger a |
| | | /// refresh internally, which will update the credential, and then retry all the queued requests with the new |
| | | /// credential. However, it is possible that some of the original requests will not return from the authentication |
| | | /// server until the refresh has completed. This is where this method comes in. |
| | | /// |
| | | /// When the authentication server rejects a credential, we need to check to make sure we haven't refreshed the |
| | | /// credential while the request was in flight. If it has already refreshed, then we don't need to trigger an |
| | | /// additional refresh. If it hasn't refreshed, then we need to refresh. |
| | | /// |
| | | /// Now that it is understood how the result of this method is used in the refresh lifecyle, let's walk through how |
| | | /// to implement it. You should return `true` in this method if the `URLRequest` is authenticated in a way that |
| | | /// matches the values in the `Credential`. In the case of OAuth2, this would mean that the Bearer token in the |
| | | /// `Authorization` header of the `URLRequest` matches the access token in the `Credential`. If it matches, then we |
| | | /// know the `Credential` was used to authenticate the `URLRequest` and should return `true`. If the Bearer token |
| | | /// did not match the access token, then you should return `false`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - urlRequest: The `URLRequest`. |
| | | /// - credential: The `Credential`. |
| | | /// |
| | | /// - Returns: `true` if the `URLRequest` is authenticated with the `Credential`, `false` otherwise. |
| | | func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: Credential) -> Bool |
| | | } |
| | | |
| | | // MARK: - |
| | | |
| | | /// Represents various authentication failures that occur when using the `AuthenticationInterceptor`. All errors are |
| | | /// still vended from Alamofire as `AFError` types. The `AuthenticationError` instances will be embedded within |
| | | /// `AFError` `.requestAdaptationFailed` or `.requestRetryFailed` cases. |
| | | public enum AuthenticationError: Error { |
| | | /// The credential was missing so the request could not be authenticated. |
| | | case missingCredential |
| | | /// The credential was refreshed too many times within the `RefreshWindow`. |
| | | case excessiveRefresh |
| | | } |
| | | |
| | | // MARK: - |
| | | |
| | | /// The `AuthenticationInterceptor` class manages the queuing and threading complexity of authenticating requests. |
| | | /// It relies on an `Authenticator` type to handle the actual `URLRequest` authentication and `Credential` refresh. |
| | | public class AuthenticationInterceptor<AuthenticatorType>: RequestInterceptor where AuthenticatorType: Authenticator { |
| | | // MARK: Typealiases |
| | | |
| | | /// Type of credential used to authenticate requests. |
| | | public typealias Credential = AuthenticatorType.Credential |
| | | |
| | | // MARK: Helper Types |
| | | |
| | | /// Type that defines a time window used to identify excessive refresh calls. When enabled, prior to executing a |
| | | /// refresh, the `AuthenticationInterceptor` compares the timestamp history of previous refresh calls against the |
| | | /// `RefreshWindow`. If more refreshes have occurred within the refresh window than allowed, the refresh is |
| | | /// cancelled and an `AuthorizationError.excessiveRefresh` error is thrown. |
| | | public struct RefreshWindow { |
| | | /// `TimeInterval` defining the duration of the time window before the current time in which the number of |
| | | /// refresh attempts is compared against `maximumAttempts`. For example, if `interval` is 30 seconds, then the |
| | | /// `RefreshWindow` represents the past 30 seconds. If more attempts occurred in the past 30 seconds than |
| | | /// `maximumAttempts`, an `.excessiveRefresh` error will be thrown. |
| | | public let interval: TimeInterval |
| | | |
| | | /// Total refresh attempts allowed within `interval` before throwing an `.excessiveRefresh` error. |
| | | public let maximumAttempts: Int |
| | | |
| | | /// Creates a `RefreshWindow` instance from the specified `interval` and `maximumAttempts`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - interval: `TimeInterval` defining the duration of the time window before the current time. |
| | | /// - maximumAttempts: The maximum attempts allowed within the `TimeInterval`. |
| | | public init(interval: TimeInterval = 30.0, maximumAttempts: Int = 5) { |
| | | self.interval = interval |
| | | self.maximumAttempts = maximumAttempts |
| | | } |
| | | } |
| | | |
| | | private struct AdaptOperation { |
| | | let urlRequest: URLRequest |
| | | let session: Session |
| | | let completion: (Result<URLRequest, Error>) -> Void |
| | | } |
| | | |
| | | private enum AdaptResult { |
| | | case adapt(Credential) |
| | | case doNotAdapt(AuthenticationError) |
| | | case adaptDeferred |
| | | } |
| | | |
| | | private struct MutableState { |
| | | var credential: Credential? |
| | | |
| | | var isRefreshing = false |
| | | var refreshTimestamps: [TimeInterval] = [] |
| | | var refreshWindow: RefreshWindow? |
| | | |
| | | var adaptOperations: [AdaptOperation] = [] |
| | | var requestsToRetry: [(RetryResult) -> Void] = [] |
| | | } |
| | | |
| | | // MARK: Properties |
| | | |
| | | /// The `Credential` used to authenticate requests. |
| | | public var credential: Credential? { |
| | | get { mutableState.credential } |
| | | set { mutableState.credential = newValue } |
| | | } |
| | | |
| | | let authenticator: AuthenticatorType |
| | | let queue = DispatchQueue(label: "org.alamofire.authentication.inspector") |
| | | |
| | | private let mutableState: Protected<MutableState> |
| | | |
| | | // MARK: Initialization |
| | | |
| | | /// Creates an `AuthenticationInterceptor` instance from the specified parameters. |
| | | /// |
| | | /// A `nil` `RefreshWindow` will result in the `AuthenticationInterceptor` not checking for excessive refresh calls. |
| | | /// It is recommended to always use a `RefreshWindow` to avoid endless refresh cycles. |
| | | /// |
| | | /// - Parameters: |
| | | /// - authenticator: The `Authenticator` type. |
| | | /// - credential: The `Credential` if it exists. `nil` by default. |
| | | /// - refreshWindow: The `RefreshWindow` used to identify excessive refresh calls. `RefreshWindow()` by default. |
| | | public init(authenticator: AuthenticatorType, |
| | | credential: Credential? = nil, |
| | | refreshWindow: RefreshWindow? = RefreshWindow()) { |
| | | self.authenticator = authenticator |
| | | mutableState = Protected(MutableState(credential: credential, refreshWindow: refreshWindow)) |
| | | } |
| | | |
| | | // MARK: Adapt |
| | | |
| | | public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | let adaptResult: AdaptResult = mutableState.write { mutableState in |
| | | // Queue the adapt operation if a refresh is already in place. |
| | | guard !mutableState.isRefreshing else { |
| | | let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion) |
| | | mutableState.adaptOperations.append(operation) |
| | | return .adaptDeferred |
| | | } |
| | | |
| | | // Throw missing credential error is the credential is missing. |
| | | guard let credential = mutableState.credential else { |
| | | let error = AuthenticationError.missingCredential |
| | | return .doNotAdapt(error) |
| | | } |
| | | |
| | | // Queue the adapt operation and trigger refresh operation if credential requires refresh. |
| | | guard !credential.requiresRefresh else { |
| | | let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion) |
| | | mutableState.adaptOperations.append(operation) |
| | | refresh(credential, for: session, insideLock: &mutableState) |
| | | return .adaptDeferred |
| | | } |
| | | |
| | | return .adapt(credential) |
| | | } |
| | | |
| | | switch adaptResult { |
| | | case let .adapt(credential): |
| | | var authenticatedRequest = urlRequest |
| | | authenticator.apply(credential, to: &authenticatedRequest) |
| | | completion(.success(authenticatedRequest)) |
| | | |
| | | case let .doNotAdapt(adaptError): |
| | | completion(.failure(adaptError)) |
| | | |
| | | case .adaptDeferred: |
| | | // No-op: adapt operation captured during refresh. |
| | | break |
| | | } |
| | | } |
| | | |
| | | // MARK: Retry |
| | | |
| | | public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) { |
| | | // Do not attempt retry if there was not an original request and response from the server. |
| | | guard let urlRequest = request.request, let response = request.response else { |
| | | completion(.doNotRetry) |
| | | return |
| | | } |
| | | |
| | | // Do not attempt retry unless the `Authenticator` verifies failure was due to authentication error (i.e. 401 status code). |
| | | guard authenticator.didRequest(urlRequest, with: response, failDueToAuthenticationError: error) else { |
| | | completion(.doNotRetry) |
| | | return |
| | | } |
| | | |
| | | // Do not attempt retry if there is no credential. |
| | | guard let credential = credential else { |
| | | let error = AuthenticationError.missingCredential |
| | | completion(.doNotRetryWithError(error)) |
| | | return |
| | | } |
| | | |
| | | // Retry the request if the `Authenticator` verifies it was authenticated with a previous credential. |
| | | guard authenticator.isRequest(urlRequest, authenticatedWith: credential) else { |
| | | completion(.retry) |
| | | return |
| | | } |
| | | |
| | | mutableState.write { mutableState in |
| | | mutableState.requestsToRetry.append(completion) |
| | | |
| | | guard !mutableState.isRefreshing else { return } |
| | | |
| | | refresh(credential, for: session, insideLock: &mutableState) |
| | | } |
| | | } |
| | | |
| | | // MARK: Refresh |
| | | |
| | | private func refresh(_ credential: Credential, for session: Session, insideLock mutableState: inout MutableState) { |
| | | guard !isRefreshExcessive(insideLock: &mutableState) else { |
| | | let error = AuthenticationError.excessiveRefresh |
| | | handleRefreshFailure(error, insideLock: &mutableState) |
| | | return |
| | | } |
| | | |
| | | mutableState.refreshTimestamps.append(ProcessInfo.processInfo.systemUptime) |
| | | mutableState.isRefreshing = true |
| | | |
| | | // Dispatch to queue to hop out of the lock in case authenticator.refresh is implemented synchronously. |
| | | queue.async { |
| | | self.authenticator.refresh(credential, for: session) { result in |
| | | self.mutableState.write { mutableState in |
| | | switch result { |
| | | case let .success(credential): |
| | | self.handleRefreshSuccess(credential, insideLock: &mutableState) |
| | | case let .failure(error): |
| | | self.handleRefreshFailure(error, insideLock: &mutableState) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private func isRefreshExcessive(insideLock mutableState: inout MutableState) -> Bool { |
| | | guard let refreshWindow = mutableState.refreshWindow else { return false } |
| | | |
| | | let refreshWindowMin = ProcessInfo.processInfo.systemUptime - refreshWindow.interval |
| | | |
| | | let refreshAttemptsWithinWindow = mutableState.refreshTimestamps.reduce(into: 0) { attempts, refreshTimestamp in |
| | | guard refreshWindowMin <= refreshTimestamp else { return } |
| | | attempts += 1 |
| | | } |
| | | |
| | | let isRefreshExcessive = refreshAttemptsWithinWindow >= refreshWindow.maximumAttempts |
| | | |
| | | return isRefreshExcessive |
| | | } |
| | | |
| | | private func handleRefreshSuccess(_ credential: Credential, insideLock mutableState: inout MutableState) { |
| | | mutableState.credential = credential |
| | | |
| | | let adaptOperations = mutableState.adaptOperations |
| | | let requestsToRetry = mutableState.requestsToRetry |
| | | |
| | | mutableState.adaptOperations.removeAll() |
| | | mutableState.requestsToRetry.removeAll() |
| | | |
| | | mutableState.isRefreshing = false |
| | | |
| | | // Dispatch to queue to hop out of the mutable state lock |
| | | queue.async { |
| | | adaptOperations.forEach { self.adapt($0.urlRequest, for: $0.session, completion: $0.completion) } |
| | | requestsToRetry.forEach { $0(.retry) } |
| | | } |
| | | } |
| | | |
| | | private func handleRefreshFailure(_ error: Error, insideLock mutableState: inout MutableState) { |
| | | let adaptOperations = mutableState.adaptOperations |
| | | let requestsToRetry = mutableState.requestsToRetry |
| | | |
| | | mutableState.adaptOperations.removeAll() |
| | | mutableState.requestsToRetry.removeAll() |
| | | |
| | | mutableState.isRefreshing = false |
| | | |
| | | // Dispatch to queue to hop out of the mutable state lock |
| | | queue.async { |
| | | adaptOperations.forEach { $0.completion(.failure(error)) } |
| | | requestsToRetry.forEach { $0(.doNotRetryWithError(error)) } |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // CachedResponseHandler.swift |
| | | // |
| | | // Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | /// A type that handles whether the data task should store the HTTP response in the cache. |
| | | public protocol CachedResponseHandler { |
| | | /// Determines whether the HTTP response should be stored in the cache. |
| | | /// |
| | | /// The `completion` closure should be passed one of three possible options: |
| | | /// |
| | | /// 1. The cached response provided by the server (this is the most common use case). |
| | | /// 2. A modified version of the cached response (you may want to modify it in some way before caching). |
| | | /// 3. A `nil` value to prevent the cached response from being stored in the cache. |
| | | /// |
| | | /// - Parameters: |
| | | /// - task: The data task whose request resulted in the cached response. |
| | | /// - response: The cached response to potentially store in the cache. |
| | | /// - completion: The closure to execute containing cached response, a modified response, or `nil`. |
| | | func dataTask(_ task: URLSessionDataTask, |
| | | willCacheResponse response: CachedURLResponse, |
| | | completion: @escaping (CachedURLResponse?) -> Void) |
| | | } |
| | | |
| | | // MARK: - |
| | | |
| | | /// `ResponseCacher` is a convenience `CachedResponseHandler` making it easy to cache, not cache, or modify a cached |
| | | /// response. |
| | | public struct ResponseCacher { |
| | | /// Defines the behavior of the `ResponseCacher` type. |
| | | public enum Behavior { |
| | | /// Stores the cached response in the cache. |
| | | case cache |
| | | /// Prevents the cached response from being stored in the cache. |
| | | case doNotCache |
| | | /// Modifies the cached response before storing it in the cache. |
| | | case modify((URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?) |
| | | } |
| | | |
| | | /// Returns a `ResponseCacher` with a `.cache` `Behavior`. |
| | | public static let cache = ResponseCacher(behavior: .cache) |
| | | /// Returns a `ResponseCacher` with a `.doNotCache` `Behavior`. |
| | | public static let doNotCache = ResponseCacher(behavior: .doNotCache) |
| | | |
| | | /// The `Behavior` of the `ResponseCacher`. |
| | | public let behavior: Behavior |
| | | |
| | | /// Creates a `ResponseCacher` instance from the `Behavior`. |
| | | /// |
| | | /// - Parameter behavior: The `Behavior`. |
| | | public init(behavior: Behavior) { |
| | | self.behavior = behavior |
| | | } |
| | | } |
| | | |
| | | extension ResponseCacher: CachedResponseHandler { |
| | | public func dataTask(_ task: URLSessionDataTask, |
| | | willCacheResponse response: CachedURLResponse, |
| | | completion: @escaping (CachedURLResponse?) -> Void) { |
| | | switch behavior { |
| | | case .cache: |
| | | completion(response) |
| | | case .doNotCache: |
| | | completion(nil) |
| | | case let .modify(closure): |
| | | let response = closure(task, response) |
| | | completion(response) |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension CachedResponseHandler where Self == ResponseCacher { |
| | | /// Provides a `ResponseCacher` which caches the response, if allowed. Equivalent to `ResponseCacher.cache`. |
| | | public static var cache: ResponseCacher { .cache } |
| | | |
| | | /// Provides a `ResponseCacher` which does not cache the response. Equivalent to `ResponseCacher.doNotCache`. |
| | | public static var doNotCache: ResponseCacher { .doNotCache } |
| | | |
| | | /// Creates a `ResponseCacher` which modifies the proposed `CachedURLResponse` using the provided closure. |
| | | /// |
| | | /// - Parameter closure: Closure used to modify the `CachedURLResponse`. |
| | | /// - Returns: The `ResponseCacher`. |
| | | public static func modify(using closure: @escaping ((URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)) -> ResponseCacher { |
| | | ResponseCacher(behavior: .modify(closure)) |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // Combine.swift |
| | | // |
| | | // Copyright (c) 2020 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | #if !((os(iOS) && (arch(i386) || arch(arm))) || os(Windows) || os(Linux) || os(Android)) |
| | | |
| | | import Combine |
| | | import Dispatch |
| | | import Foundation |
| | | |
| | | // MARK: - DataRequest / UploadRequest |
| | | |
| | | /// A Combine `Publisher` that publishes the `DataResponse<Value, AFError>` of the provided `DataRequest`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public struct DataResponsePublisher<Value>: Publisher { |
| | | public typealias Output = DataResponse<Value, AFError> |
| | | public typealias Failure = Never |
| | | |
| | | private typealias Handler = (@escaping (_ response: DataResponse<Value, AFError>) -> Void) -> DataRequest |
| | | |
| | | private let request: DataRequest |
| | | private let responseHandler: Handler |
| | | |
| | | /// Creates an instance which will serialize responses using the provided `ResponseSerializer`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - request: `DataRequest` for which to publish the response. |
| | | /// - queue: `DispatchQueue` on which the `DataResponse` value will be published. `.main` by default. |
| | | /// - serializer: `ResponseSerializer` used to produce the published `DataResponse`. |
| | | public init<Serializer: ResponseSerializer>(_ request: DataRequest, queue: DispatchQueue, serializer: Serializer) |
| | | where Value == Serializer.SerializedObject { |
| | | self.request = request |
| | | responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) } |
| | | } |
| | | |
| | | /// Creates an instance which will serialize responses using the provided `DataResponseSerializerProtocol`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - request: `DataRequest` for which to publish the response. |
| | | /// - queue: `DispatchQueue` on which the `DataResponse` value will be published. `.main` by default. |
| | | /// - serializer: `DataResponseSerializerProtocol` used to produce the published `DataResponse`. |
| | | public init<Serializer: DataResponseSerializerProtocol>(_ request: DataRequest, |
| | | queue: DispatchQueue, |
| | | serializer: Serializer) |
| | | where Value == Serializer.SerializedObject { |
| | | self.request = request |
| | | responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) } |
| | | } |
| | | |
| | | /// Publishes only the `Result` of the `DataResponse` value. |
| | | /// |
| | | /// - Returns: The `AnyPublisher` publishing the `Result<Value, AFError>` value. |
| | | public func result() -> AnyPublisher<Result<Value, AFError>, Never> { |
| | | map(\.result).eraseToAnyPublisher() |
| | | } |
| | | |
| | | /// Publishes the `Result` of the `DataResponse` as a single `Value` or fail with the `AFError` instance. |
| | | /// |
| | | /// - Returns: The `AnyPublisher<Value, AFError>` publishing the stream. |
| | | public func value() -> AnyPublisher<Value, AFError> { |
| | | setFailureType(to: AFError.self).flatMap(\.result.publisher).eraseToAnyPublisher() |
| | | } |
| | | |
| | | public func receive<S>(subscriber: S) where S: Subscriber, DataResponsePublisher.Failure == S.Failure, DataResponsePublisher.Output == S.Input { |
| | | subscriber.receive(subscription: Inner(request: request, |
| | | responseHandler: responseHandler, |
| | | downstream: subscriber)) |
| | | } |
| | | |
| | | private final class Inner<Downstream: Subscriber>: Subscription |
| | | where Downstream.Input == Output { |
| | | typealias Failure = Downstream.Failure |
| | | |
| | | private let downstream: Protected<Downstream?> |
| | | private let request: DataRequest |
| | | private let responseHandler: Handler |
| | | |
| | | init(request: DataRequest, responseHandler: @escaping Handler, downstream: Downstream) { |
| | | self.request = request |
| | | self.responseHandler = responseHandler |
| | | self.downstream = Protected(downstream) |
| | | } |
| | | |
| | | func request(_ demand: Subscribers.Demand) { |
| | | assert(demand > 0) |
| | | |
| | | guard let downstream = downstream.read({ $0 }) else { return } |
| | | |
| | | self.downstream.write(nil) |
| | | responseHandler { response in |
| | | _ = downstream.receive(response) |
| | | downstream.receive(completion: .finished) |
| | | }.resume() |
| | | } |
| | | |
| | | func cancel() { |
| | | request.cancel() |
| | | downstream.write(nil) |
| | | } |
| | | } |
| | | } |
| | | |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | extension DataResponsePublisher where Value == Data? { |
| | | /// Creates an instance which publishes a `DataResponse<Data?, AFError>` value without serialization. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public init(_ request: DataRequest, queue: DispatchQueue) { |
| | | self.request = request |
| | | responseHandler = { request.response(queue: queue, completionHandler: $0) } |
| | | } |
| | | } |
| | | |
| | | extension DataRequest { |
| | | /// Creates a `DataResponsePublisher` for this instance using the given `ResponseSerializer` and `DispatchQueue`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - serializer: `ResponseSerializer` used to serialize response `Data`. |
| | | /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. |
| | | /// |
| | | /// - Returns: The `DataResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishResponse<Serializer: ResponseSerializer, T>(using serializer: Serializer, on queue: DispatchQueue = .main) -> DataResponsePublisher<T> |
| | | where Serializer.SerializedObject == T { |
| | | DataResponsePublisher(self, queue: queue, serializer: serializer) |
| | | } |
| | | |
| | | /// Creates a `DataResponsePublisher` for this instance and uses a `DataResponseSerializer` to serialize the |
| | | /// response. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. |
| | | /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()` |
| | | /// by default. |
| | | /// - emptyResponseCodes: `Set<Int>` of HTTP status codes for which empty responses are allowed. `[204, 205]` by |
| | | /// default. |
| | | /// - emptyRequestMethods: `Set<HTTPMethod>` of `HTTPMethod`s for which empty responses are allowed, regardless of |
| | | /// status code. `[.head]` by default. |
| | | /// - Returns: The `DataResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishData(queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher<Data> { |
| | | publishResponse(using: DataResponseSerializer(dataPreprocessor: preprocessor, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyRequestMethods), |
| | | on: queue) |
| | | } |
| | | |
| | | /// Creates a `DataResponsePublisher` for this instance and uses a `StringResponseSerializer` to serialize the |
| | | /// response. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. |
| | | /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()` |
| | | /// by default. |
| | | /// - encoding: `String.Encoding` to parse the response. `nil` by default, in which case the encoding |
| | | /// will be determined by the server response, falling back to the default HTTP character |
| | | /// set, `ISO-8859-1`. |
| | | /// - emptyResponseCodes: `Set<Int>` of HTTP status codes for which empty responses are allowed. `[204, 205]` by |
| | | /// default. |
| | | /// - emptyRequestMethods: `Set<HTTPMethod>` of `HTTPMethod`s for which empty responses are allowed, regardless of |
| | | /// status code. `[.head]` by default. |
| | | /// |
| | | /// - Returns: The `DataResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishString(queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | encoding: String.Encoding? = nil, |
| | | emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher<String> { |
| | | publishResponse(using: StringResponseSerializer(dataPreprocessor: preprocessor, |
| | | encoding: encoding, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyRequestMethods), |
| | | on: queue) |
| | | } |
| | | |
| | | @_disfavoredOverload |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | @available(*, deprecated, message: "Renamed publishDecodable(type:queue:preprocessor:decoder:emptyResponseCodes:emptyRequestMethods).") |
| | | public func publishDecodable<T: Decodable>(type: T.Type = T.self, |
| | | queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes, |
| | | emptyResponseMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DataResponsePublisher<T> { |
| | | publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, |
| | | decoder: decoder, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyResponseMethods), |
| | | on: queue) |
| | | } |
| | | |
| | | /// Creates a `DataResponsePublisher` for this instance and uses a `DecodableResponseSerializer` to serialize the |
| | | /// response. |
| | | /// |
| | | /// - Parameters: |
| | | /// - type: `Decodable` type to which to decode response `Data`. Inferred from the context by |
| | | /// default. |
| | | /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. |
| | | /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. |
| | | /// `PassthroughPreprocessor()` by default. |
| | | /// - decoder: `DataDecoder` instance used to decode response `Data`. `JSONDecoder()` by default. |
| | | /// - emptyResponseCodes: `Set<Int>` of HTTP status codes for which empty responses are allowed. `[204, 205]` by |
| | | /// default. |
| | | /// - emptyRequestMethods: `Set<HTTPMethod>` of `HTTPMethod`s for which empty responses are allowed, regardless of |
| | | /// status code. `[.head]` by default. |
| | | /// |
| | | /// - Returns: The `DataResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishDecodable<T: Decodable>(type: T.Type = T.self, |
| | | queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DataResponsePublisher<T> { |
| | | publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, |
| | | decoder: decoder, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyRequestMethods), |
| | | on: queue) |
| | | } |
| | | |
| | | /// Creates a `DataResponsePublisher` for this instance which does not serialize the response before publishing. |
| | | /// |
| | | /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. |
| | | /// |
| | | /// - Returns: The `DataResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishUnserialized(queue: DispatchQueue = .main) -> DataResponsePublisher<Data?> { |
| | | DataResponsePublisher(self, queue: queue) |
| | | } |
| | | } |
| | | |
| | | // A Combine `Publisher` that publishes a sequence of `Stream<Value, AFError>` values received by the provided `DataStreamRequest`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public struct DataStreamPublisher<Value>: Publisher { |
| | | public typealias Output = DataStreamRequest.Stream<Value, AFError> |
| | | public typealias Failure = Never |
| | | |
| | | private typealias Handler = (@escaping DataStreamRequest.Handler<Value, AFError>) -> DataStreamRequest |
| | | |
| | | private let request: DataStreamRequest |
| | | private let streamHandler: Handler |
| | | |
| | | /// Creates an instance which will serialize responses using the provided `DataStreamSerializer`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - request: `DataStreamRequest` for which to publish the response. |
| | | /// - queue: `DispatchQueue` on which the `Stream<Value, AFError>` values will be published. `.main` by |
| | | /// default. |
| | | /// - serializer: `DataStreamSerializer` used to produce the published `Stream<Value, AFError>` values. |
| | | public init<Serializer: DataStreamSerializer>(_ request: DataStreamRequest, queue: DispatchQueue, serializer: Serializer) |
| | | where Value == Serializer.SerializedObject { |
| | | self.request = request |
| | | streamHandler = { request.responseStream(using: serializer, on: queue, stream: $0) } |
| | | } |
| | | |
| | | /// Publishes only the `Result` of the `DataStreamRequest.Stream`'s `Event`s. |
| | | /// |
| | | /// - Returns: The `AnyPublisher` publishing the `Result<Value, AFError>` value. |
| | | public func result() -> AnyPublisher<Result<Value, AFError>, Never> { |
| | | compactMap { stream in |
| | | switch stream.event { |
| | | case let .stream(result): |
| | | return result |
| | | // If the stream has completed with an error, send the error value downstream as a `.failure`. |
| | | case let .complete(completion): |
| | | return completion.error.map(Result.failure) |
| | | } |
| | | } |
| | | .eraseToAnyPublisher() |
| | | } |
| | | |
| | | /// Publishes the streamed values of the `DataStreamRequest.Stream` as a sequence of `Value` or fail with the |
| | | /// `AFError` instance. |
| | | /// |
| | | /// - Returns: The `AnyPublisher<Value, AFError>` publishing the stream. |
| | | public func value() -> AnyPublisher<Value, AFError> { |
| | | result().setFailureType(to: AFError.self).flatMap(\.publisher).eraseToAnyPublisher() |
| | | } |
| | | |
| | | public func receive<S>(subscriber: S) where S: Subscriber, DataStreamPublisher.Failure == S.Failure, DataStreamPublisher.Output == S.Input { |
| | | subscriber.receive(subscription: Inner(request: request, |
| | | streamHandler: streamHandler, |
| | | downstream: subscriber)) |
| | | } |
| | | |
| | | private final class Inner<Downstream: Subscriber>: Subscription |
| | | where Downstream.Input == Output { |
| | | typealias Failure = Downstream.Failure |
| | | |
| | | private let downstream: Protected<Downstream?> |
| | | private let request: DataStreamRequest |
| | | private let streamHandler: Handler |
| | | |
| | | init(request: DataStreamRequest, streamHandler: @escaping Handler, downstream: Downstream) { |
| | | self.request = request |
| | | self.streamHandler = streamHandler |
| | | self.downstream = Protected(downstream) |
| | | } |
| | | |
| | | func request(_ demand: Subscribers.Demand) { |
| | | assert(demand > 0) |
| | | |
| | | guard let downstream = downstream.read({ $0 }) else { return } |
| | | |
| | | self.downstream.write(nil) |
| | | streamHandler { stream in |
| | | _ = downstream.receive(stream) |
| | | if case .complete = stream.event { |
| | | downstream.receive(completion: .finished) |
| | | } |
| | | }.resume() |
| | | } |
| | | |
| | | func cancel() { |
| | | request.cancel() |
| | | downstream.write(nil) |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension DataStreamRequest { |
| | | /// Creates a `DataStreamPublisher` for this instance using the given `DataStreamSerializer` and `DispatchQueue`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - serializer: `DataStreamSerializer` used to serialize the streamed `Data`. |
| | | /// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default. |
| | | /// - Returns: The `DataStreamPublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishStream<Serializer: DataStreamSerializer>(using serializer: Serializer, |
| | | on queue: DispatchQueue = .main) -> DataStreamPublisher<Serializer.SerializedObject> { |
| | | DataStreamPublisher(self, queue: queue, serializer: serializer) |
| | | } |
| | | |
| | | /// Creates a `DataStreamPublisher` for this instance which uses a `PassthroughStreamSerializer` to stream `Data` |
| | | /// unserialized. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default. |
| | | /// - Returns: The `DataStreamPublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishData(queue: DispatchQueue = .main) -> DataStreamPublisher<Data> { |
| | | publishStream(using: PassthroughStreamSerializer(), on: queue) |
| | | } |
| | | |
| | | /// Creates a `DataStreamPublisher` for this instance which uses a `StringStreamSerializer` to serialize stream |
| | | /// `Data` values into `String` values. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default. |
| | | /// - Returns: The `DataStreamPublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishString(queue: DispatchQueue = .main) -> DataStreamPublisher<String> { |
| | | publishStream(using: StringStreamSerializer(), on: queue) |
| | | } |
| | | |
| | | /// Creates a `DataStreamPublisher` for this instance which uses a `DecodableStreamSerializer` with the provided |
| | | /// parameters to serialize stream `Data` values into the provided type. |
| | | /// |
| | | /// - Parameters: |
| | | /// - type: `Decodable` type to which to decode stream `Data`. Inferred from the context by default. |
| | | /// - queue: `DispatchQueue` on which the `DataRequest.Stream` values will be published. `.main` by default. |
| | | /// - decoder: `DataDecoder` instance used to decode stream `Data`. `JSONDecoder()` by default. |
| | | /// - preprocessor: `DataPreprocessor` which filters incoming stream `Data` before serialization. |
| | | /// `PassthroughPreprocessor()` by default. |
| | | /// - Returns: The `DataStreamPublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishDecodable<T: Decodable>(type: T.Type = T.self, |
| | | queue: DispatchQueue = .main, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | preprocessor: DataPreprocessor = PassthroughPreprocessor()) -> DataStreamPublisher<T> { |
| | | publishStream(using: DecodableStreamSerializer(decoder: decoder, |
| | | dataPreprocessor: preprocessor), |
| | | on: queue) |
| | | } |
| | | } |
| | | |
| | | /// A Combine `Publisher` that publishes the `DownloadResponse<Value, AFError>` of the provided `DownloadRequest`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public struct DownloadResponsePublisher<Value>: Publisher { |
| | | public typealias Output = DownloadResponse<Value, AFError> |
| | | public typealias Failure = Never |
| | | |
| | | private typealias Handler = (@escaping (_ response: DownloadResponse<Value, AFError>) -> Void) -> DownloadRequest |
| | | |
| | | private let request: DownloadRequest |
| | | private let responseHandler: Handler |
| | | |
| | | /// Creates an instance which will serialize responses using the provided `ResponseSerializer`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - request: `DownloadRequest` for which to publish the response. |
| | | /// - queue: `DispatchQueue` on which the `DownloadResponse` value will be published. `.main` by default. |
| | | /// - serializer: `ResponseSerializer` used to produce the published `DownloadResponse`. |
| | | public init<Serializer: ResponseSerializer>(_ request: DownloadRequest, queue: DispatchQueue, serializer: Serializer) |
| | | where Value == Serializer.SerializedObject { |
| | | self.request = request |
| | | responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) } |
| | | } |
| | | |
| | | /// Creates an instance which will serialize responses using the provided `DownloadResponseSerializerProtocol` value. |
| | | /// |
| | | /// - Parameters: |
| | | /// - request: `DownloadRequest` for which to publish the response. |
| | | /// - queue: `DispatchQueue` on which the `DataResponse` value will be published. `.main` by default. |
| | | /// - serializer: `DownloadResponseSerializerProtocol` used to produce the published `DownloadResponse`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public init<Serializer: DownloadResponseSerializerProtocol>(_ request: DownloadRequest, |
| | | queue: DispatchQueue, |
| | | serializer: Serializer) |
| | | where Value == Serializer.SerializedObject { |
| | | self.request = request |
| | | responseHandler = { request.response(queue: queue, responseSerializer: serializer, completionHandler: $0) } |
| | | } |
| | | |
| | | /// Publishes only the `Result` of the `DownloadResponse` value. |
| | | /// |
| | | /// - Returns: The `AnyPublisher` publishing the `Result<Value, AFError>` value. |
| | | public func result() -> AnyPublisher<Result<Value, AFError>, Never> { |
| | | map(\.result).eraseToAnyPublisher() |
| | | } |
| | | |
| | | /// Publishes the `Result` of the `DownloadResponse` as a single `Value` or fail with the `AFError` instance. |
| | | /// |
| | | /// - Returns: The `AnyPublisher<Value, AFError>` publishing the stream. |
| | | public func value() -> AnyPublisher<Value, AFError> { |
| | | setFailureType(to: AFError.self).flatMap(\.result.publisher).eraseToAnyPublisher() |
| | | } |
| | | |
| | | public func receive<S>(subscriber: S) where S: Subscriber, DownloadResponsePublisher.Failure == S.Failure, DownloadResponsePublisher.Output == S.Input { |
| | | subscriber.receive(subscription: Inner(request: request, |
| | | responseHandler: responseHandler, |
| | | downstream: subscriber)) |
| | | } |
| | | |
| | | private final class Inner<Downstream: Subscriber>: Subscription |
| | | where Downstream.Input == Output { |
| | | typealias Failure = Downstream.Failure |
| | | |
| | | private let downstream: Protected<Downstream?> |
| | | private let request: DownloadRequest |
| | | private let responseHandler: Handler |
| | | |
| | | init(request: DownloadRequest, responseHandler: @escaping Handler, downstream: Downstream) { |
| | | self.request = request |
| | | self.responseHandler = responseHandler |
| | | self.downstream = Protected(downstream) |
| | | } |
| | | |
| | | func request(_ demand: Subscribers.Demand) { |
| | | assert(demand > 0) |
| | | |
| | | guard let downstream = downstream.read({ $0 }) else { return } |
| | | |
| | | self.downstream.write(nil) |
| | | responseHandler { response in |
| | | _ = downstream.receive(response) |
| | | downstream.receive(completion: .finished) |
| | | }.resume() |
| | | } |
| | | |
| | | func cancel() { |
| | | request.cancel() |
| | | downstream.write(nil) |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension DownloadRequest { |
| | | /// Creates a `DownloadResponsePublisher` for this instance using the given `ResponseSerializer` and `DispatchQueue`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - serializer: `ResponseSerializer` used to serialize the response `Data` from disk. |
| | | /// - queue: `DispatchQueue` on which the `DownloadResponse` will be published.`.main` by default. |
| | | /// |
| | | /// - Returns: The `DownloadResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishResponse<Serializer: ResponseSerializer, T>(using serializer: Serializer, on queue: DispatchQueue = .main) -> DownloadResponsePublisher<T> |
| | | where Serializer.SerializedObject == T { |
| | | DownloadResponsePublisher(self, queue: queue, serializer: serializer) |
| | | } |
| | | |
| | | /// Creates a `DownloadResponsePublisher` for this instance using the given `DownloadResponseSerializerProtocol` and |
| | | /// `DispatchQueue`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - serializer: `DownloadResponseSerializer` used to serialize the response `Data` from disk. |
| | | /// - queue: `DispatchQueue` on which the `DownloadResponse` will be published.`.main` by default. |
| | | /// |
| | | /// - Returns: The `DownloadResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishResponse<Serializer: DownloadResponseSerializerProtocol, T>(using serializer: Serializer, on queue: DispatchQueue = .main) -> DownloadResponsePublisher<T> |
| | | where Serializer.SerializedObject == T { |
| | | DownloadResponsePublisher(self, queue: queue, serializer: serializer) |
| | | } |
| | | |
| | | /// Creates a `DownloadResponsePublisher` for this instance and uses a `URLResponseSerializer` to serialize the |
| | | /// response. |
| | | /// |
| | | /// - Parameter queue: `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default. |
| | | /// |
| | | /// - Returns: The `DownloadResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishURL(queue: DispatchQueue = .main) -> DownloadResponsePublisher<URL> { |
| | | publishResponse(using: URLResponseSerializer(), on: queue) |
| | | } |
| | | |
| | | /// Creates a `DownloadResponsePublisher` for this instance and uses a `DataResponseSerializer` to serialize the |
| | | /// response. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default. |
| | | /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()` |
| | | /// by default. |
| | | /// - emptyResponseCodes: `Set<Int>` of HTTP status codes for which empty responses are allowed. `[204, 205]` by |
| | | /// default. |
| | | /// - emptyRequestMethods: `Set<HTTPMethod>` of `HTTPMethod`s for which empty responses are allowed, regardless of |
| | | /// status code. `[.head]` by default. |
| | | /// |
| | | /// - Returns: The `DownloadResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishData(queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher<Data> { |
| | | publishResponse(using: DataResponseSerializer(dataPreprocessor: preprocessor, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyRequestMethods), |
| | | on: queue) |
| | | } |
| | | |
| | | /// Creates a `DownloadResponsePublisher` for this instance and uses a `StringResponseSerializer` to serialize the |
| | | /// response. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. |
| | | /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. `PassthroughPreprocessor()` |
| | | /// by default. |
| | | /// - encoding: `String.Encoding` to parse the response. `nil` by default, in which case the encoding |
| | | /// will be determined by the server response, falling back to the default HTTP character |
| | | /// set, `ISO-8859-1`. |
| | | /// - emptyResponseCodes: `Set<Int>` of HTTP status codes for which empty responses are allowed. `[204, 205]` by |
| | | /// default. |
| | | /// - emptyRequestMethods: `Set<HTTPMethod>` of `HTTPMethod`s for which empty responses are allowed, regardless of |
| | | /// status code. `[.head]` by default. |
| | | /// |
| | | /// - Returns: The `DownloadResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishString(queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | encoding: String.Encoding? = nil, |
| | | emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher<String> { |
| | | publishResponse(using: StringResponseSerializer(dataPreprocessor: preprocessor, |
| | | encoding: encoding, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyRequestMethods), |
| | | on: queue) |
| | | } |
| | | |
| | | @_disfavoredOverload |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | @available(*, deprecated, message: "Renamed publishDecodable(type:queue:preprocessor:decoder:emptyResponseCodes:emptyRequestMethods).") |
| | | public func publishDecodable<T: Decodable>(type: T.Type = T.self, |
| | | queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes, |
| | | emptyResponseMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DownloadResponsePublisher<T> { |
| | | publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, |
| | | decoder: decoder, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyResponseMethods), |
| | | on: queue) |
| | | } |
| | | |
| | | /// Creates a `DownloadResponsePublisher` for this instance and uses a `DecodableResponseSerializer` to serialize |
| | | /// the response. |
| | | /// |
| | | /// - Parameters: |
| | | /// - type: `Decodable` type to which to decode response `Data`. Inferred from the context by default. |
| | | /// - queue: `DispatchQueue` on which the `DataResponse` will be published. `.main` by default. |
| | | /// - preprocessor: `DataPreprocessor` which filters the `Data` before serialization. |
| | | /// `PassthroughPreprocessor()` by default. |
| | | /// - decoder: `DataDecoder` instance used to decode response `Data`. `JSONDecoder()` by default. |
| | | /// - emptyResponseCodes: `Set<Int>` of HTTP status codes for which empty responses are allowed. `[204, 205]` by |
| | | /// default. |
| | | /// - emptyRequestMethods: `Set<HTTPMethod>` of `HTTPMethod`s for which empty responses are allowed, regardless |
| | | /// of status code. `[.head]` by default. |
| | | /// |
| | | /// - Returns: The `DownloadResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishDecodable<T: Decodable>(type: T.Type = T.self, |
| | | queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DownloadResponsePublisher<T> { |
| | | publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, |
| | | decoder: decoder, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyRequestMethods), |
| | | on: queue) |
| | | } |
| | | } |
| | | |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | extension DownloadResponsePublisher where Value == URL? { |
| | | /// Creates an instance which publishes a `DownloadResponse<URL?, AFError>` value without serialization. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public init(_ request: DownloadRequest, queue: DispatchQueue) { |
| | | self.request = request |
| | | responseHandler = { request.response(queue: queue, completionHandler: $0) } |
| | | } |
| | | } |
| | | |
| | | extension DownloadRequest { |
| | | /// Creates a `DownloadResponsePublisher` for this instance which does not serialize the response before publishing. |
| | | /// |
| | | /// - Parameter queue: `DispatchQueue` on which the `DownloadResponse` will be published. `.main` by default. |
| | | /// |
| | | /// - Returns: The `DownloadResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishUnserialized(on queue: DispatchQueue = .main) -> DownloadResponsePublisher<URL?> { |
| | | DownloadResponsePublisher(self, queue: queue) |
| | | } |
| | | } |
| | | |
| | | #endif |
New file |
| | |
| | | // |
| | | // Concurrency.swift |
| | | // |
| | | // Copyright (c) 2021 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | #if compiler(>=5.6.0) && canImport(_Concurrency) |
| | | |
| | | import Foundation |
| | | |
| | | // MARK: - Request Event Streams |
| | | |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | extension Request { |
| | | /// Creates a `StreamOf<Progress>` for the instance's upload progress. |
| | | /// |
| | | /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. |
| | | /// |
| | | /// - Returns: The `StreamOf<Progress>`. |
| | | public func uploadProgress(bufferingPolicy: StreamOf<Progress>.BufferingPolicy = .unbounded) -> StreamOf<Progress> { |
| | | stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in |
| | | uploadProgress(queue: underlyingQueue) { progress in |
| | | continuation.yield(progress) |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// Creates a `StreamOf<Progress>` for the instance's download progress. |
| | | /// |
| | | /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. |
| | | /// |
| | | /// - Returns: The `StreamOf<Progress>`. |
| | | public func downloadProgress(bufferingPolicy: StreamOf<Progress>.BufferingPolicy = .unbounded) -> StreamOf<Progress> { |
| | | stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in |
| | | downloadProgress(queue: underlyingQueue) { progress in |
| | | continuation.yield(progress) |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// Creates a `StreamOf<URLRequest>` for the `URLRequest`s produced for the instance. |
| | | /// |
| | | /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. |
| | | /// |
| | | /// - Returns: The `StreamOf<URLRequest>`. |
| | | public func urlRequests(bufferingPolicy: StreamOf<URLRequest>.BufferingPolicy = .unbounded) -> StreamOf<URLRequest> { |
| | | stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in |
| | | onURLRequestCreation(on: underlyingQueue) { request in |
| | | continuation.yield(request) |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// Creates a `StreamOf<URLSessionTask>` for the `URLSessionTask`s produced for the instance. |
| | | /// |
| | | /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. |
| | | /// |
| | | /// - Returns: The `StreamOf<URLSessionTask>`. |
| | | public func urlSessionTasks(bufferingPolicy: StreamOf<URLSessionTask>.BufferingPolicy = .unbounded) -> StreamOf<URLSessionTask> { |
| | | stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in |
| | | onURLSessionTaskCreation(on: underlyingQueue) { task in |
| | | continuation.yield(task) |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// Creates a `StreamOf<String>` for the cURL descriptions produced for the instance. |
| | | /// |
| | | /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. |
| | | /// |
| | | /// - Returns: The `StreamOf<String>`. |
| | | public func cURLDescriptions(bufferingPolicy: StreamOf<String>.BufferingPolicy = .unbounded) -> StreamOf<String> { |
| | | stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in |
| | | cURLDescription(on: underlyingQueue) { description in |
| | | continuation.yield(description) |
| | | } |
| | | } |
| | | } |
| | | |
| | | fileprivate func stream<T>(of type: T.Type = T.self, |
| | | bufferingPolicy: StreamOf<T>.BufferingPolicy = .unbounded, |
| | | yielder: @escaping (StreamOf<T>.Continuation) -> Void) -> StreamOf<T> { |
| | | StreamOf<T>(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in |
| | | yielder(continuation) |
| | | // Must come after serializers run in order to catch retry progress. |
| | | onFinish { |
| | | continuation.finish() |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // MARK: - DataTask |
| | | |
| | | /// Value used to `await` a `DataResponse` and associated values. |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | public struct DataTask<Value> { |
| | | /// `DataResponse` produced by the `DataRequest` and its response handler. |
| | | public var response: DataResponse<Value, AFError> { |
| | | get async { |
| | | if shouldAutomaticallyCancel { |
| | | return await withTaskCancellationHandler { |
| | | await task.value |
| | | } onCancel: { |
| | | cancel() |
| | | } |
| | | } else { |
| | | return await task.value |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// `Result` of any response serialization performed for the `response`. |
| | | public var result: Result<Value, AFError> { |
| | | get async { await response.result } |
| | | } |
| | | |
| | | /// `Value` returned by the `response`. |
| | | public var value: Value { |
| | | get async throws { |
| | | try await result.get() |
| | | } |
| | | } |
| | | |
| | | private let request: DataRequest |
| | | private let task: Task<DataResponse<Value, AFError>, Never> |
| | | private let shouldAutomaticallyCancel: Bool |
| | | |
| | | fileprivate init(request: DataRequest, task: Task<DataResponse<Value, AFError>, Never>, shouldAutomaticallyCancel: Bool) { |
| | | self.request = request |
| | | self.task = task |
| | | self.shouldAutomaticallyCancel = shouldAutomaticallyCancel |
| | | } |
| | | |
| | | /// Cancel the underlying `DataRequest` and `Task`. |
| | | public func cancel() { |
| | | task.cancel() |
| | | } |
| | | |
| | | /// Resume the underlying `DataRequest`. |
| | | public func resume() { |
| | | request.resume() |
| | | } |
| | | |
| | | /// Suspend the underlying `DataRequest`. |
| | | public func suspend() { |
| | | request.suspend() |
| | | } |
| | | } |
| | | |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | extension DataRequest { |
| | | /// Creates a `StreamOf<HTTPURLResponse>` for the instance's responses. |
| | | /// |
| | | /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. |
| | | /// |
| | | /// - Returns: The `StreamOf<HTTPURLResponse>`. |
| | | public func httpResponses(bufferingPolicy: StreamOf<HTTPURLResponse>.BufferingPolicy = .unbounded) -> StreamOf<HTTPURLResponse> { |
| | | stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in |
| | | onHTTPResponse(on: underlyingQueue) { response in |
| | | continuation.yield(response) |
| | | } |
| | | } |
| | | } |
| | | |
| | | #if swift(>=5.7) |
| | | /// Sets an async closure returning a `Request.ResponseDisposition`, called whenever the `DataRequest` produces an |
| | | /// `HTTPURLResponse`. |
| | | /// |
| | | /// - Note: Most requests will only produce a single response for each outgoing attempt (initial + retries). |
| | | /// However, some types of response may trigger multiple `HTTPURLResponse`s, such as multipart streams, |
| | | /// where responses after the first will contain the part headers. |
| | | /// |
| | | /// - Parameters: |
| | | /// - handler: Async closure executed when a new `HTTPURLResponse` is received and returning a |
| | | /// `ResponseDisposition` value. This value determines whether to continue the request or cancel it as |
| | | /// if `cancel()` had been called on the instance. Note, this closure is called on an arbitrary thread, |
| | | /// so any synchronous calls in it will execute in that context. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @_disfavoredOverload |
| | | @discardableResult |
| | | public func onHTTPResponse( |
| | | perform handler: @escaping @Sendable (_ response: HTTPURLResponse) async -> ResponseDisposition |
| | | ) -> Self { |
| | | onHTTPResponse(on: underlyingQueue) { response, completionHandler in |
| | | Task { |
| | | let disposition = await handler(response) |
| | | completionHandler(disposition) |
| | | } |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | /// Sets an async closure called whenever the `DataRequest` produces an `HTTPURLResponse`. |
| | | /// |
| | | /// - Note: Most requests will only produce a single response for each outgoing attempt (initial + retries). |
| | | /// However, some types of response may trigger multiple `HTTPURLResponse`s, such as multipart streams, |
| | | /// where responses after the first will contain the part headers. |
| | | /// |
| | | /// - Parameters: |
| | | /// - handler: Async closure executed when a new `HTTPURLResponse` is received. Note, this closure is called on an |
| | | /// arbitrary thread, so any synchronous calls in it will execute in that context. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func onHTTPResponse(perform handler: @escaping @Sendable (_ response: HTTPURLResponse) async -> Void) -> Self { |
| | | onHTTPResponse { response in |
| | | await handler(response) |
| | | return .allow |
| | | } |
| | | |
| | | return self |
| | | } |
| | | #endif |
| | | |
| | | /// Creates a `DataTask` to `await` a `Data` value. |
| | | /// |
| | | /// - Parameters: |
| | | /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the |
| | | /// enclosing async context is cancelled. Only applies to `DataTask`'s async |
| | | /// properties. `true` by default. |
| | | /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before completion. |
| | | /// - emptyResponseCodes: HTTP response codes for which empty responses are allowed. `[204, 205]` by default. |
| | | /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. |
| | | /// |
| | | /// - Returns: The `DataTask`. |
| | | public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DataTask<Data> { |
| | | serializingResponse(using: DataResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyRequestMethods), |
| | | automaticallyCancelling: shouldAutomaticallyCancel) |
| | | } |
| | | |
| | | /// Creates a `DataTask` to `await` serialization of a `Decodable` value. |
| | | /// |
| | | /// - Parameters: |
| | | /// - type: `Decodable` type to decode from response data. |
| | | /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the |
| | | /// enclosing async context is cancelled. Only applies to `DataTask`'s async |
| | | /// properties. `true` by default. |
| | | /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer. |
| | | /// `PassthroughPreprocessor()` by default. |
| | | /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. |
| | | /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. |
| | | /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. |
| | | /// |
| | | /// - Returns: The `DataTask`. |
| | | public func serializingDecodable<Value: Decodable>(_ type: Value.Type = Value.self, |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<Value>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<Value>.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<Value>.defaultEmptyRequestMethods) -> DataTask<Value> { |
| | | serializingResponse(using: DecodableResponseSerializer<Value>(dataPreprocessor: dataPreprocessor, |
| | | decoder: decoder, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyRequestMethods), |
| | | automaticallyCancelling: shouldAutomaticallyCancel) |
| | | } |
| | | |
| | | /// Creates a `DataTask` to `await` serialization of a `String` value. |
| | | /// |
| | | /// - Parameters: |
| | | /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the |
| | | /// enclosing async context is cancelled. Only applies to `DataTask`'s async |
| | | /// properties. `true` by default. |
| | | /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer. |
| | | /// `PassthroughPreprocessor()` by default. |
| | | /// - encoding: `String.Encoding` to use during serialization. Defaults to `nil`, in which case |
| | | /// the encoding will be determined from the server response, falling back to the |
| | | /// default HTTP character set, `ISO-8859-1`. |
| | | /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. |
| | | /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. |
| | | /// |
| | | /// - Returns: The `DataTask`. |
| | | public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | encoding: String.Encoding? = nil, |
| | | emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> DataTask<String> { |
| | | serializingResponse(using: StringResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | | encoding: encoding, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyRequestMethods), |
| | | automaticallyCancelling: shouldAutomaticallyCancel) |
| | | } |
| | | |
| | | /// Creates a `DataTask` to `await` serialization using the provided `ResponseSerializer` instance. |
| | | /// |
| | | /// - Parameters: |
| | | /// - serializer: `ResponseSerializer` responsible for serializing the request, response, and data. |
| | | /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the |
| | | /// enclosing async context is cancelled. Only applies to `DataTask`'s async |
| | | /// properties. `true` by default. |
| | | /// |
| | | /// - Returns: The `DataTask`. |
| | | public func serializingResponse<Serializer: ResponseSerializer>(using serializer: Serializer, |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool = true) |
| | | -> DataTask<Serializer.SerializedObject> { |
| | | dataTask(automaticallyCancelling: shouldAutomaticallyCancel) { [self] in |
| | | response(queue: underlyingQueue, |
| | | responseSerializer: serializer, |
| | | completionHandler: $0) |
| | | } |
| | | } |
| | | |
| | | /// Creates a `DataTask` to `await` serialization using the provided `DataResponseSerializerProtocol` instance. |
| | | /// |
| | | /// - Parameters: |
| | | /// - serializer: `DataResponseSerializerProtocol` responsible for serializing the request, |
| | | /// response, and data. |
| | | /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the |
| | | /// enclosing async context is cancelled. Only applies to `DataTask`'s async |
| | | /// properties. `true` by default. |
| | | /// |
| | | /// - Returns: The `DataTask`. |
| | | public func serializingResponse<Serializer: DataResponseSerializerProtocol>(using serializer: Serializer, |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool = true) |
| | | -> DataTask<Serializer.SerializedObject> { |
| | | dataTask(automaticallyCancelling: shouldAutomaticallyCancel) { [self] in |
| | | response(queue: underlyingQueue, |
| | | responseSerializer: serializer, |
| | | completionHandler: $0) |
| | | } |
| | | } |
| | | |
| | | private func dataTask<Value>(automaticallyCancelling shouldAutomaticallyCancel: Bool, |
| | | forResponse onResponse: @escaping (@escaping (DataResponse<Value, AFError>) -> Void) -> Void) |
| | | -> DataTask<Value> { |
| | | let task = Task { |
| | | await withTaskCancellationHandler { |
| | | await withCheckedContinuation { continuation in |
| | | onResponse { |
| | | continuation.resume(returning: $0) |
| | | } |
| | | } |
| | | } onCancel: { |
| | | self.cancel() |
| | | } |
| | | } |
| | | |
| | | return DataTask<Value>(request: self, task: task, shouldAutomaticallyCancel: shouldAutomaticallyCancel) |
| | | } |
| | | } |
| | | |
| | | // MARK: - DownloadTask |
| | | |
| | | /// Value used to `await` a `DownloadResponse` and associated values. |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | public struct DownloadTask<Value> { |
| | | /// `DownloadResponse` produced by the `DownloadRequest` and its response handler. |
| | | public var response: DownloadResponse<Value, AFError> { |
| | | get async { |
| | | if shouldAutomaticallyCancel { |
| | | return await withTaskCancellationHandler { |
| | | await task.value |
| | | } onCancel: { |
| | | cancel() |
| | | } |
| | | } else { |
| | | return await task.value |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// `Result` of any response serialization performed for the `response`. |
| | | public var result: Result<Value, AFError> { |
| | | get async { await response.result } |
| | | } |
| | | |
| | | /// `Value` returned by the `response`. |
| | | public var value: Value { |
| | | get async throws { |
| | | try await result.get() |
| | | } |
| | | } |
| | | |
| | | private let task: Task<AFDownloadResponse<Value>, Never> |
| | | private let request: DownloadRequest |
| | | private let shouldAutomaticallyCancel: Bool |
| | | |
| | | fileprivate init(request: DownloadRequest, task: Task<AFDownloadResponse<Value>, Never>, shouldAutomaticallyCancel: Bool) { |
| | | self.request = request |
| | | self.task = task |
| | | self.shouldAutomaticallyCancel = shouldAutomaticallyCancel |
| | | } |
| | | |
| | | /// Cancel the underlying `DownloadRequest` and `Task`. |
| | | public func cancel() { |
| | | task.cancel() |
| | | } |
| | | |
| | | /// Resume the underlying `DownloadRequest`. |
| | | public func resume() { |
| | | request.resume() |
| | | } |
| | | |
| | | /// Suspend the underlying `DownloadRequest`. |
| | | public func suspend() { |
| | | request.suspend() |
| | | } |
| | | } |
| | | |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | extension DownloadRequest { |
| | | /// Creates a `DownloadTask` to `await` a `Data` value. |
| | | /// |
| | | /// - Parameters: |
| | | /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the |
| | | /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async |
| | | /// properties. `true` by default. |
| | | /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before completion. |
| | | /// - emptyResponseCodes: HTTP response codes for which empty responses are allowed. `[204, 205]` by default. |
| | | /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. |
| | | /// |
| | | /// - Returns: The `DownloadTask`. |
| | | public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask<Data> { |
| | | serializingDownload(using: DataResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyRequestMethods), |
| | | automaticallyCancelling: shouldAutomaticallyCancel) |
| | | } |
| | | |
| | | /// Creates a `DownloadTask` to `await` serialization of a `Decodable` value. |
| | | /// |
| | | /// - Note: This serializer reads the entire response into memory before parsing. |
| | | /// |
| | | /// - Parameters: |
| | | /// - type: `Decodable` type to decode from response data. |
| | | /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the |
| | | /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async |
| | | /// properties. `true` by default. |
| | | /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer. |
| | | /// `PassthroughPreprocessor()` by default. |
| | | /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. |
| | | /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. |
| | | /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. |
| | | /// |
| | | /// - Returns: The `DownloadTask`. |
| | | public func serializingDecodable<Value: Decodable>(_ type: Value.Type = Value.self, |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<Value>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<Value>.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<Value>.defaultEmptyRequestMethods) -> DownloadTask<Value> { |
| | | serializingDownload(using: DecodableResponseSerializer<Value>(dataPreprocessor: dataPreprocessor, |
| | | decoder: decoder, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyRequestMethods), |
| | | automaticallyCancelling: shouldAutomaticallyCancel) |
| | | } |
| | | |
| | | /// Creates a `DownloadTask` to `await` serialization of the downloaded file's `URL` on disk. |
| | | /// |
| | | /// - Parameters: |
| | | /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the |
| | | /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async |
| | | /// properties. `true` by default. |
| | | /// |
| | | /// - Returns: The `DownloadTask`. |
| | | public func serializingDownloadedFileURL(automaticallyCancelling shouldAutomaticallyCancel: Bool = true) -> DownloadTask<URL> { |
| | | serializingDownload(using: URLResponseSerializer(), |
| | | automaticallyCancelling: shouldAutomaticallyCancel) |
| | | } |
| | | |
| | | /// Creates a `DownloadTask` to `await` serialization of a `String` value. |
| | | /// |
| | | /// - Parameters: |
| | | /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the |
| | | /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async |
| | | /// properties. `true` by default. |
| | | /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the |
| | | /// serializer. `PassthroughPreprocessor()` by default. |
| | | /// - encoding: `String.Encoding` to use during serialization. Defaults to `nil`, in which case |
| | | /// the encoding will be determined from the server response, falling back to the |
| | | /// default HTTP character set, `ISO-8859-1`. |
| | | /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. |
| | | /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. |
| | | /// |
| | | /// - Returns: The `DownloadTask`. |
| | | public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | encoding: String.Encoding? = nil, |
| | | emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask<String> { |
| | | serializingDownload(using: StringResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | | encoding: encoding, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | | emptyRequestMethods: emptyRequestMethods), |
| | | automaticallyCancelling: shouldAutomaticallyCancel) |
| | | } |
| | | |
| | | /// Creates a `DownloadTask` to `await` serialization using the provided `ResponseSerializer` instance. |
| | | /// |
| | | /// - Parameters: |
| | | /// - serializer: `ResponseSerializer` responsible for serializing the request, response, and data. |
| | | /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the |
| | | /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async |
| | | /// properties. `true` by default. |
| | | /// |
| | | /// - Returns: The `DownloadTask`. |
| | | public func serializingDownload<Serializer: ResponseSerializer>(using serializer: Serializer, |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool = true) |
| | | -> DownloadTask<Serializer.SerializedObject> { |
| | | downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) { [self] in |
| | | response(queue: underlyingQueue, |
| | | responseSerializer: serializer, |
| | | completionHandler: $0) |
| | | } |
| | | } |
| | | |
| | | /// Creates a `DownloadTask` to `await` serialization using the provided `DownloadResponseSerializerProtocol` |
| | | /// instance. |
| | | /// |
| | | /// - Parameters: |
| | | /// - serializer: `DownloadResponseSerializerProtocol` responsible for serializing the request, |
| | | /// response, and data. |
| | | /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the |
| | | /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async |
| | | /// properties. `true` by default. |
| | | /// |
| | | /// - Returns: The `DownloadTask`. |
| | | public func serializingDownload<Serializer: DownloadResponseSerializerProtocol>(using serializer: Serializer, |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool = true) |
| | | -> DownloadTask<Serializer.SerializedObject> { |
| | | downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) { [self] in |
| | | response(queue: underlyingQueue, |
| | | responseSerializer: serializer, |
| | | completionHandler: $0) |
| | | } |
| | | } |
| | | |
| | | private func downloadTask<Value>(automaticallyCancelling shouldAutomaticallyCancel: Bool, |
| | | forResponse onResponse: @escaping (@escaping (DownloadResponse<Value, AFError>) -> Void) -> Void) |
| | | -> DownloadTask<Value> { |
| | | let task = Task { |
| | | await withTaskCancellationHandler { |
| | | await withCheckedContinuation { continuation in |
| | | onResponse { |
| | | continuation.resume(returning: $0) |
| | | } |
| | | } |
| | | } onCancel: { |
| | | self.cancel() |
| | | } |
| | | } |
| | | |
| | | return DownloadTask<Value>(request: self, task: task, shouldAutomaticallyCancel: shouldAutomaticallyCancel) |
| | | } |
| | | } |
| | | |
| | | // MARK: - DataStreamTask |
| | | |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | public struct DataStreamTask { |
| | | // Type of created streams. |
| | | public typealias Stream<Success, Failure: Error> = StreamOf<DataStreamRequest.Stream<Success, Failure>> |
| | | |
| | | private let request: DataStreamRequest |
| | | |
| | | fileprivate init(request: DataStreamRequest) { |
| | | self.request = request |
| | | } |
| | | |
| | | /// Creates a `Stream` of `Data` values from the underlying `DataStreamRequest`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled |
| | | /// which observation of the stream stops. `true` by default. |
| | | /// - bufferingPolicy: ` BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. |
| | | /// |
| | | /// - Returns: The `Stream`. |
| | | public func streamingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, bufferingPolicy: Stream<Data, Never>.BufferingPolicy = .unbounded) -> Stream<Data, Never> { |
| | | createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in |
| | | request.responseStream(on: .streamCompletionQueue(forRequestID: request.id), stream: onStream) |
| | | } |
| | | } |
| | | |
| | | /// Creates a `Stream` of `UTF-8` `String`s from the underlying `DataStreamRequest`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled |
| | | /// which observation of the stream stops. `true` by default. |
| | | /// - bufferingPolicy: ` BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. |
| | | /// - Returns: |
| | | public func streamingStrings(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, bufferingPolicy: Stream<String, Never>.BufferingPolicy = .unbounded) -> Stream<String, Never> { |
| | | createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in |
| | | request.responseStreamString(on: .streamCompletionQueue(forRequestID: request.id), stream: onStream) |
| | | } |
| | | } |
| | | |
| | | /// Creates a `Stream` of `Decodable` values from the underlying `DataStreamRequest`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - type: `Decodable` type to be serialized from stream payloads. |
| | | /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled |
| | | /// which observation of the stream stops. `true` by default. |
| | | /// - bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. |
| | | /// |
| | | /// - Returns: The `Stream`. |
| | | public func streamingDecodables<T>(_ type: T.Type = T.self, |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | bufferingPolicy: Stream<T, AFError>.BufferingPolicy = .unbounded) |
| | | -> Stream<T, AFError> where T: Decodable { |
| | | streamingResponses(serializedUsing: DecodableStreamSerializer<T>(), |
| | | automaticallyCancelling: shouldAutomaticallyCancel, |
| | | bufferingPolicy: bufferingPolicy) |
| | | } |
| | | |
| | | /// Creates a `Stream` of values using the provided `DataStreamSerializer` from the underlying `DataStreamRequest`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - serializer: `DataStreamSerializer` to use to serialize incoming `Data`. |
| | | /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled |
| | | /// which observation of the stream stops. `true` by default. |
| | | /// - bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. |
| | | /// |
| | | /// - Returns: The `Stream`. |
| | | public func streamingResponses<Serializer: DataStreamSerializer>(serializedUsing serializer: Serializer, |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | bufferingPolicy: Stream<Serializer.SerializedObject, AFError>.BufferingPolicy = .unbounded) |
| | | -> Stream<Serializer.SerializedObject, AFError> { |
| | | createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in |
| | | request.responseStream(using: serializer, |
| | | on: .streamCompletionQueue(forRequestID: request.id), |
| | | stream: onStream) |
| | | } |
| | | } |
| | | |
| | | private func createStream<Success, Failure: Error>(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | bufferingPolicy: Stream<Success, Failure>.BufferingPolicy = .unbounded, |
| | | forResponse onResponse: @escaping (@escaping (DataStreamRequest.Stream<Success, Failure>) -> Void) -> Void) |
| | | -> Stream<Success, Failure> { |
| | | StreamOf(bufferingPolicy: bufferingPolicy) { |
| | | guard shouldAutomaticallyCancel, |
| | | request.isInitialized || request.isResumed || request.isSuspended else { return } |
| | | |
| | | cancel() |
| | | } builder: { continuation in |
| | | onResponse { stream in |
| | | continuation.yield(stream) |
| | | if case .complete = stream.event { |
| | | continuation.finish() |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// Cancel the underlying `DataStreamRequest`. |
| | | public func cancel() { |
| | | request.cancel() |
| | | } |
| | | |
| | | /// Resume the underlying `DataStreamRequest`. |
| | | public func resume() { |
| | | request.resume() |
| | | } |
| | | |
| | | /// Suspend the underlying `DataStreamRequest`. |
| | | public func suspend() { |
| | | request.suspend() |
| | | } |
| | | } |
| | | |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | extension DataStreamRequest { |
| | | /// Creates a `StreamOf<HTTPURLResponse>` for the instance's responses. |
| | | /// |
| | | /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. |
| | | /// |
| | | /// - Returns: The `StreamOf<HTTPURLResponse>`. |
| | | public func httpResponses(bufferingPolicy: StreamOf<HTTPURLResponse>.BufferingPolicy = .unbounded) -> StreamOf<HTTPURLResponse> { |
| | | stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in |
| | | onHTTPResponse(on: underlyingQueue) { response in |
| | | continuation.yield(response) |
| | | } |
| | | } |
| | | } |
| | | |
| | | #if swift(>=5.7) |
| | | /// Sets an async closure returning a `Request.ResponseDisposition`, called whenever the `DataStreamRequest` |
| | | /// produces an `HTTPURLResponse`. |
| | | /// |
| | | /// - Note: Most requests will only produce a single response for each outgoing attempt (initial + retries). |
| | | /// However, some types of response may trigger multiple `HTTPURLResponse`s, such as multipart streams, |
| | | /// where responses after the first will contain the part headers. |
| | | /// |
| | | /// - Parameters: |
| | | /// - handler: Async closure executed when a new `HTTPURLResponse` is received and returning a |
| | | /// `ResponseDisposition` value. This value determines whether to continue the request or cancel it as |
| | | /// if `cancel()` had been called on the instance. Note, this closure is called on an arbitrary thread, |
| | | /// so any synchronous calls in it will execute in that context. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @_disfavoredOverload |
| | | @discardableResult |
| | | public func onHTTPResponse(perform handler: @escaping @Sendable (HTTPURLResponse) async -> ResponseDisposition) -> Self { |
| | | onHTTPResponse(on: underlyingQueue) { response, completionHandler in |
| | | Task { |
| | | let disposition = await handler(response) |
| | | completionHandler(disposition) |
| | | } |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | /// Sets an async closure called whenever the `DataStreamRequest` produces an `HTTPURLResponse`. |
| | | /// |
| | | /// - Note: Most requests will only produce a single response for each outgoing attempt (initial + retries). |
| | | /// However, some types of response may trigger multiple `HTTPURLResponse`s, such as multipart streams, |
| | | /// where responses after the first will contain the part headers. |
| | | /// |
| | | /// - Parameters: |
| | | /// - handler: Async closure executed when a new `HTTPURLResponse` is received. Note, this closure is called on an |
| | | /// arbitrary thread, so any synchronous calls in it will execute in that context. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func onHTTPResponse(perform handler: @escaping @Sendable (HTTPURLResponse) async -> Void) -> Self { |
| | | onHTTPResponse { response in |
| | | await handler(response) |
| | | return .allow |
| | | } |
| | | |
| | | return self |
| | | } |
| | | #endif |
| | | |
| | | /// Creates a `DataStreamTask` used to `await` streams of serialized values. |
| | | /// |
| | | /// - Returns: The `DataStreamTask`. |
| | | public func streamTask() -> DataStreamTask { |
| | | DataStreamTask(request: self) |
| | | } |
| | | } |
| | | |
| | | extension DispatchQueue { |
| | | fileprivate static let singleEventQueue = DispatchQueue(label: "org.alamofire.concurrencySingleEventQueue", |
| | | attributes: .concurrent) |
| | | |
| | | fileprivate static func streamCompletionQueue(forRequestID id: UUID) -> DispatchQueue { |
| | | DispatchQueue(label: "org.alamofire.concurrencyStreamCompletionQueue-\(id)", target: .singleEventQueue) |
| | | } |
| | | } |
| | | |
| | | /// An asynchronous sequence generated from an underlying `AsyncStream`. Only produced by Alamofire. |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | public struct StreamOf<Element>: AsyncSequence { |
| | | public typealias AsyncIterator = Iterator |
| | | public typealias BufferingPolicy = AsyncStream<Element>.Continuation.BufferingPolicy |
| | | fileprivate typealias Continuation = AsyncStream<Element>.Continuation |
| | | |
| | | private let bufferingPolicy: BufferingPolicy |
| | | private let onTermination: (() -> Void)? |
| | | private let builder: (Continuation) -> Void |
| | | |
| | | fileprivate init(bufferingPolicy: BufferingPolicy = .unbounded, |
| | | onTermination: (() -> Void)? = nil, |
| | | builder: @escaping (Continuation) -> Void) { |
| | | self.bufferingPolicy = bufferingPolicy |
| | | self.onTermination = onTermination |
| | | self.builder = builder |
| | | } |
| | | |
| | | public func makeAsyncIterator() -> Iterator { |
| | | var continuation: AsyncStream<Element>.Continuation? |
| | | let stream = AsyncStream<Element>(bufferingPolicy: bufferingPolicy) { innerContinuation in |
| | | continuation = innerContinuation |
| | | builder(innerContinuation) |
| | | } |
| | | |
| | | return Iterator(iterator: stream.makeAsyncIterator()) { |
| | | continuation?.finish() |
| | | onTermination?() |
| | | } |
| | | } |
| | | |
| | | public struct Iterator: AsyncIteratorProtocol { |
| | | private final class Token { |
| | | private let onDeinit: () -> Void |
| | | |
| | | init(onDeinit: @escaping () -> Void) { |
| | | self.onDeinit = onDeinit |
| | | } |
| | | |
| | | deinit { |
| | | onDeinit() |
| | | } |
| | | } |
| | | |
| | | private var iterator: AsyncStream<Element>.AsyncIterator |
| | | private let token: Token |
| | | |
| | | init(iterator: AsyncStream<Element>.AsyncIterator, onCancellation: @escaping () -> Void) { |
| | | self.iterator = iterator |
| | | token = Token(onDeinit: onCancellation) |
| | | } |
| | | |
| | | public mutating func next() async -> Element? { |
| | | await iterator.next() |
| | | } |
| | | } |
| | | } |
| | | |
| | | #endif |
New file |
| | |
| | | // |
| | | // DispatchQueue+Alamofire.swift |
| | | // |
| | | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Dispatch |
| | | import Foundation |
| | | |
| | | extension DispatchQueue { |
| | | /// Execute the provided closure after a `TimeInterval`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - delay: `TimeInterval` to delay execution. |
| | | /// - closure: Closure to execute. |
| | | func after(_ delay: TimeInterval, execute closure: @escaping () -> Void) { |
| | | asyncAfter(deadline: .now() + delay, execute: closure) |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // EventMonitor.swift |
| | | // |
| | | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | /// Protocol outlining the lifetime events inside Alamofire. It includes both events received from the various |
| | | /// `URLSession` delegate protocols as well as various events from the lifetime of `Request` and its subclasses. |
| | | public protocol EventMonitor { |
| | | /// The `DispatchQueue` onto which Alamofire's root `CompositeEventMonitor` will dispatch events. `.main` by default. |
| | | var queue: DispatchQueue { get } |
| | | |
| | | // MARK: - URLSession Events |
| | | |
| | | // MARK: URLSessionDelegate Events |
| | | |
| | | /// Event called during `URLSessionDelegate`'s `urlSession(_:didBecomeInvalidWithError:)` method. |
| | | func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) |
| | | |
| | | // MARK: URLSessionTaskDelegate Events |
| | | |
| | | /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didReceive:completionHandler:)` method. |
| | | func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) |
| | | |
| | | /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` method. |
| | | func urlSession(_ session: URLSession, |
| | | task: URLSessionTask, |
| | | didSendBodyData bytesSent: Int64, |
| | | totalBytesSent: Int64, |
| | | totalBytesExpectedToSend: Int64) |
| | | |
| | | /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:needNewBodyStream:)` method. |
| | | func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) |
| | | |
| | | /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` method. |
| | | func urlSession(_ session: URLSession, |
| | | task: URLSessionTask, |
| | | willPerformHTTPRedirection response: HTTPURLResponse, |
| | | newRequest request: URLRequest) |
| | | |
| | | /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didFinishCollecting:)` method. |
| | | func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) |
| | | |
| | | /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didCompleteWithError:)` method. |
| | | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) |
| | | |
| | | /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:taskIsWaitingForConnectivity:)` method. |
| | | func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) |
| | | |
| | | // MARK: URLSessionDataDelegate Events |
| | | |
| | | /// Event called during `URLSessionDataDelegate`'s `urlSession(_:dataTask:didReceive:completionHandler:)` method. |
| | | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) |
| | | |
| | | /// Event called during `URLSessionDataDelegate`'s `urlSession(_:dataTask:didReceive:)` method. |
| | | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) |
| | | |
| | | /// Event called during `URLSessionDataDelegate`'s `urlSession(_:dataTask:willCacheResponse:completionHandler:)` method. |
| | | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse) |
| | | |
| | | // MARK: URLSessionDownloadDelegate Events |
| | | |
| | | /// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)` method. |
| | | func urlSession(_ session: URLSession, |
| | | downloadTask: URLSessionDownloadTask, |
| | | didResumeAtOffset fileOffset: Int64, |
| | | expectedTotalBytes: Int64) |
| | | |
| | | /// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)` method. |
| | | func urlSession(_ session: URLSession, |
| | | downloadTask: URLSessionDownloadTask, |
| | | didWriteData bytesWritten: Int64, |
| | | totalBytesWritten: Int64, |
| | | totalBytesExpectedToWrite: Int64) |
| | | |
| | | /// Event called during `URLSessionDownloadDelegate`'s `urlSession(_:downloadTask:didFinishDownloadingTo:)` method. |
| | | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) |
| | | |
| | | // MARK: - Request Events |
| | | |
| | | /// Event called when a `URLRequest` is first created for a `Request`. If a `RequestAdapter` is active, the |
| | | /// `URLRequest` will be adapted before being issued. |
| | | func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) |
| | | |
| | | /// Event called when the attempt to create a `URLRequest` from a `Request`'s original `URLRequestConvertible` value fails. |
| | | func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) |
| | | |
| | | /// Event called when a `RequestAdapter` adapts the `Request`'s initial `URLRequest`. |
| | | func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) |
| | | |
| | | /// Event called when a `RequestAdapter` fails to adapt the `Request`'s initial `URLRequest`. |
| | | func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: AFError) |
| | | |
| | | /// Event called when a final `URLRequest` is created for a `Request`. |
| | | func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) |
| | | |
| | | /// Event called when a `URLSessionTask` subclass instance is created for a `Request`. |
| | | func request(_ request: Request, didCreateTask task: URLSessionTask) |
| | | |
| | | /// Event called when a `Request` receives a `URLSessionTaskMetrics` value. |
| | | func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) |
| | | |
| | | /// Event called when a `Request` fails due to an error created by Alamofire. e.g. When certificate pinning fails. |
| | | func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) |
| | | |
| | | /// Event called when a `Request`'s task completes, possibly with an error. A `Request` may receive this event |
| | | /// multiple times if it is retried. |
| | | func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) |
| | | |
| | | /// Event called when a `Request` is about to be retried. |
| | | func requestIsRetrying(_ request: Request) |
| | | |
| | | /// Event called when a `Request` finishes and response serializers are being called. |
| | | func requestDidFinish(_ request: Request) |
| | | |
| | | /// Event called when a `Request` receives a `resume` call. |
| | | func requestDidResume(_ request: Request) |
| | | |
| | | /// Event called when a `Request`'s associated `URLSessionTask` is resumed. |
| | | func request(_ request: Request, didResumeTask task: URLSessionTask) |
| | | |
| | | /// Event called when a `Request` receives a `suspend` call. |
| | | func requestDidSuspend(_ request: Request) |
| | | |
| | | /// Event called when a `Request`'s associated `URLSessionTask` is suspended. |
| | | func request(_ request: Request, didSuspendTask task: URLSessionTask) |
| | | |
| | | /// Event called when a `Request` receives a `cancel` call. |
| | | func requestDidCancel(_ request: Request) |
| | | |
| | | /// Event called when a `Request`'s associated `URLSessionTask` is cancelled. |
| | | func request(_ request: Request, didCancelTask task: URLSessionTask) |
| | | |
| | | // MARK: DataRequest Events |
| | | |
| | | /// Event called when a `DataRequest` calls a `Validation`. |
| | | func request(_ request: DataRequest, |
| | | didValidateRequest urlRequest: URLRequest?, |
| | | response: HTTPURLResponse, |
| | | data: Data?, |
| | | withResult result: Request.ValidationResult) |
| | | |
| | | /// Event called when a `DataRequest` creates a `DataResponse<Data?>` value without calling a `ResponseSerializer`. |
| | | func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>) |
| | | |
| | | /// Event called when a `DataRequest` calls a `ResponseSerializer` and creates a generic `DataResponse<Value, AFError>`. |
| | | func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) |
| | | |
| | | // MARK: DataStreamRequest Events |
| | | |
| | | /// Event called when a `DataStreamRequest` calls a `Validation` closure. |
| | | /// |
| | | /// - Parameters: |
| | | /// - request: `DataStreamRequest` which is calling the `Validation`. |
| | | /// - urlRequest: `URLRequest` of the request being validated. |
| | | /// - response: `HTTPURLResponse` of the request being validated. |
| | | /// - result: Produced `ValidationResult`. |
| | | func request(_ request: DataStreamRequest, |
| | | didValidateRequest urlRequest: URLRequest?, |
| | | response: HTTPURLResponse, |
| | | withResult result: Request.ValidationResult) |
| | | |
| | | /// Event called when a `DataStreamSerializer` produces a value from streamed `Data`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - request: `DataStreamRequest` for which the value was serialized. |
| | | /// - result: `Result` of the serialization attempt. |
| | | func request<Value>(_ request: DataStreamRequest, didParseStream result: Result<Value, AFError>) |
| | | |
| | | // MARK: UploadRequest Events |
| | | |
| | | /// Event called when an `UploadRequest` creates its `Uploadable` value, indicating the type of upload it represents. |
| | | func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) |
| | | |
| | | /// Event called when an `UploadRequest` failed to create its `Uploadable` value due to an error. |
| | | func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) |
| | | |
| | | /// Event called when an `UploadRequest` provides the `InputStream` from its `Uploadable` value. This only occurs if |
| | | /// the `InputStream` does not wrap a `Data` value or file `URL`. |
| | | func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) |
| | | |
| | | // MARK: DownloadRequest Events |
| | | |
| | | /// Event called when a `DownloadRequest`'s `URLSessionDownloadTask` finishes and the temporary file has been moved. |
| | | func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result<URL, AFError>) |
| | | |
| | | /// Event called when a `DownloadRequest`'s `Destination` closure is called and creates the destination URL the |
| | | /// downloaded file will be moved to. |
| | | func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) |
| | | |
| | | /// Event called when a `DownloadRequest` calls a `Validation`. |
| | | func request(_ request: DownloadRequest, |
| | | didValidateRequest urlRequest: URLRequest?, |
| | | response: HTTPURLResponse, |
| | | fileURL: URL?, |
| | | withResult result: Request.ValidationResult) |
| | | |
| | | /// Event called when a `DownloadRequest` creates a `DownloadResponse<URL?, AFError>` without calling a `ResponseSerializer`. |
| | | func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse<URL?, AFError>) |
| | | |
| | | /// Event called when a `DownloadRequest` calls a `DownloadResponseSerializer` and creates a generic `DownloadResponse<Value, AFError>` |
| | | func request<Value>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, AFError>) |
| | | } |
| | | |
| | | extension EventMonitor { |
| | | /// The default queue on which `CompositeEventMonitor`s will call the `EventMonitor` methods. `.main` by default. |
| | | public var queue: DispatchQueue { .main } |
| | | |
| | | // MARK: Default Implementations |
| | | |
| | | public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {} |
| | | public func urlSession(_ session: URLSession, |
| | | task: URLSessionTask, |
| | | didReceive challenge: URLAuthenticationChallenge) {} |
| | | public func urlSession(_ session: URLSession, |
| | | task: URLSessionTask, |
| | | didSendBodyData bytesSent: Int64, |
| | | totalBytesSent: Int64, |
| | | totalBytesExpectedToSend: Int64) {} |
| | | public func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) {} |
| | | public func urlSession(_ session: URLSession, |
| | | task: URLSessionTask, |
| | | willPerformHTTPRedirection response: HTTPURLResponse, |
| | | newRequest request: URLRequest) {} |
| | | public func urlSession(_ session: URLSession, |
| | | task: URLSessionTask, |
| | | didFinishCollecting metrics: URLSessionTaskMetrics) {} |
| | | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {} |
| | | public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {} |
| | | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) {} |
| | | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {} |
| | | public func urlSession(_ session: URLSession, |
| | | dataTask: URLSessionDataTask, |
| | | willCacheResponse proposedResponse: CachedURLResponse) {} |
| | | public func urlSession(_ session: URLSession, |
| | | downloadTask: URLSessionDownloadTask, |
| | | didResumeAtOffset fileOffset: Int64, |
| | | expectedTotalBytes: Int64) {} |
| | | public func urlSession(_ session: URLSession, |
| | | downloadTask: URLSessionDownloadTask, |
| | | didWriteData bytesWritten: Int64, |
| | | totalBytesWritten: Int64, |
| | | totalBytesExpectedToWrite: Int64) {} |
| | | public func urlSession(_ session: URLSession, |
| | | downloadTask: URLSessionDownloadTask, |
| | | didFinishDownloadingTo location: URL) {} |
| | | public func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) {} |
| | | public func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) {} |
| | | public func request(_ request: Request, |
| | | didAdaptInitialRequest initialRequest: URLRequest, |
| | | to adaptedRequest: URLRequest) {} |
| | | public func request(_ request: Request, |
| | | didFailToAdaptURLRequest initialRequest: URLRequest, |
| | | withError error: AFError) {} |
| | | public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) {} |
| | | public func request(_ request: Request, didCreateTask task: URLSessionTask) {} |
| | | public func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) {} |
| | | public func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) {} |
| | | public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) {} |
| | | public func requestIsRetrying(_ request: Request) {} |
| | | public func requestDidFinish(_ request: Request) {} |
| | | public func requestDidResume(_ request: Request) {} |
| | | public func request(_ request: Request, didResumeTask task: URLSessionTask) {} |
| | | public func requestDidSuspend(_ request: Request) {} |
| | | public func request(_ request: Request, didSuspendTask task: URLSessionTask) {} |
| | | public func requestDidCancel(_ request: Request) {} |
| | | public func request(_ request: Request, didCancelTask task: URLSessionTask) {} |
| | | public func request(_ request: DataRequest, |
| | | didValidateRequest urlRequest: URLRequest?, |
| | | response: HTTPURLResponse, |
| | | data: Data?, |
| | | withResult result: Request.ValidationResult) {} |
| | | public func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>) {} |
| | | public func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {} |
| | | public func request(_ request: DataStreamRequest, |
| | | didValidateRequest urlRequest: URLRequest?, |
| | | response: HTTPURLResponse, |
| | | withResult result: Request.ValidationResult) {} |
| | | public func request<Value>(_ request: DataStreamRequest, didParseStream result: Result<Value, AFError>) {} |
| | | public func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) {} |
| | | public func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) {} |
| | | public func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) {} |
| | | public func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result<URL, AFError>) {} |
| | | public func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) {} |
| | | public func request(_ request: DownloadRequest, |
| | | didValidateRequest urlRequest: URLRequest?, |
| | | response: HTTPURLResponse, |
| | | fileURL: URL?, |
| | | withResult result: Request.ValidationResult) {} |
| | | public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse<URL?, AFError>) {} |
| | | public func request<Value>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, AFError>) {} |
| | | } |
| | | |
| | | /// An `EventMonitor` which can contain multiple `EventMonitor`s and calls their methods on their queues. |
| | | public final class CompositeEventMonitor: EventMonitor { |
| | | public let queue = DispatchQueue(label: "org.alamofire.compositeEventMonitor", qos: .utility) |
| | | |
| | | let monitors: [EventMonitor] |
| | | |
| | | init(monitors: [EventMonitor]) { |
| | | self.monitors = monitors |
| | | } |
| | | |
| | | func performEvent(_ event: @escaping (EventMonitor) -> Void) { |
| | | queue.async { |
| | | for monitor in self.monitors { |
| | | monitor.queue.async { event(monitor) } |
| | | } |
| | | } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { |
| | | performEvent { $0.urlSession(session, didBecomeInvalidWithError: error) } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, |
| | | task: URLSessionTask, |
| | | didReceive challenge: URLAuthenticationChallenge) { |
| | | performEvent { $0.urlSession(session, task: task, didReceive: challenge) } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, |
| | | task: URLSessionTask, |
| | | didSendBodyData bytesSent: Int64, |
| | | totalBytesSent: Int64, |
| | | totalBytesExpectedToSend: Int64) { |
| | | performEvent { |
| | | $0.urlSession(session, |
| | | task: task, |
| | | didSendBodyData: bytesSent, |
| | | totalBytesSent: totalBytesSent, |
| | | totalBytesExpectedToSend: totalBytesExpectedToSend) |
| | | } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) { |
| | | performEvent { |
| | | $0.urlSession(session, taskNeedsNewBodyStream: task) |
| | | } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, |
| | | task: URLSessionTask, |
| | | willPerformHTTPRedirection response: HTTPURLResponse, |
| | | newRequest request: URLRequest) { |
| | | performEvent { |
| | | $0.urlSession(session, |
| | | task: task, |
| | | willPerformHTTPRedirection: response, |
| | | newRequest: request) |
| | | } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { |
| | | performEvent { $0.urlSession(session, task: task, didFinishCollecting: metrics) } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { |
| | | performEvent { $0.urlSession(session, task: task, didCompleteWithError: error) } |
| | | } |
| | | |
| | | @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) |
| | | public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { |
| | | performEvent { $0.urlSession(session, taskIsWaitingForConnectivity: task) } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) { |
| | | performEvent { $0.urlSession(session, dataTask: dataTask, didReceive: response) } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { |
| | | performEvent { $0.urlSession(session, dataTask: dataTask, didReceive: data) } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, |
| | | dataTask: URLSessionDataTask, |
| | | willCacheResponse proposedResponse: CachedURLResponse) { |
| | | performEvent { $0.urlSession(session, dataTask: dataTask, willCacheResponse: proposedResponse) } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, |
| | | downloadTask: URLSessionDownloadTask, |
| | | didResumeAtOffset fileOffset: Int64, |
| | | expectedTotalBytes: Int64) { |
| | | performEvent { |
| | | $0.urlSession(session, |
| | | downloadTask: downloadTask, |
| | | didResumeAtOffset: fileOffset, |
| | | expectedTotalBytes: expectedTotalBytes) |
| | | } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, |
| | | downloadTask: URLSessionDownloadTask, |
| | | didWriteData bytesWritten: Int64, |
| | | totalBytesWritten: Int64, |
| | | totalBytesExpectedToWrite: Int64) { |
| | | performEvent { |
| | | $0.urlSession(session, |
| | | downloadTask: downloadTask, |
| | | didWriteData: bytesWritten, |
| | | totalBytesWritten: totalBytesWritten, |
| | | totalBytesExpectedToWrite: totalBytesExpectedToWrite) |
| | | } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, |
| | | downloadTask: URLSessionDownloadTask, |
| | | didFinishDownloadingTo location: URL) { |
| | | performEvent { $0.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location) } |
| | | } |
| | | |
| | | public func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) { |
| | | performEvent { $0.request(request, didCreateInitialURLRequest: urlRequest) } |
| | | } |
| | | |
| | | public func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) { |
| | | performEvent { $0.request(request, didFailToCreateURLRequestWithError: error) } |
| | | } |
| | | |
| | | public func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) { |
| | | performEvent { $0.request(request, didAdaptInitialRequest: initialRequest, to: adaptedRequest) } |
| | | } |
| | | |
| | | public func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: AFError) { |
| | | performEvent { $0.request(request, didFailToAdaptURLRequest: initialRequest, withError: error) } |
| | | } |
| | | |
| | | public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) { |
| | | performEvent { $0.request(request, didCreateURLRequest: urlRequest) } |
| | | } |
| | | |
| | | public func request(_ request: Request, didCreateTask task: URLSessionTask) { |
| | | performEvent { $0.request(request, didCreateTask: task) } |
| | | } |
| | | |
| | | public func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) { |
| | | performEvent { $0.request(request, didGatherMetrics: metrics) } |
| | | } |
| | | |
| | | public func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) { |
| | | performEvent { $0.request(request, didFailTask: task, earlyWithError: error) } |
| | | } |
| | | |
| | | public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) { |
| | | performEvent { $0.request(request, didCompleteTask: task, with: error) } |
| | | } |
| | | |
| | | public func requestIsRetrying(_ request: Request) { |
| | | performEvent { $0.requestIsRetrying(request) } |
| | | } |
| | | |
| | | public func requestDidFinish(_ request: Request) { |
| | | performEvent { $0.requestDidFinish(request) } |
| | | } |
| | | |
| | | public func requestDidResume(_ request: Request) { |
| | | performEvent { $0.requestDidResume(request) } |
| | | } |
| | | |
| | | public func request(_ request: Request, didResumeTask task: URLSessionTask) { |
| | | performEvent { $0.request(request, didResumeTask: task) } |
| | | } |
| | | |
| | | public func requestDidSuspend(_ request: Request) { |
| | | performEvent { $0.requestDidSuspend(request) } |
| | | } |
| | | |
| | | public func request(_ request: Request, didSuspendTask task: URLSessionTask) { |
| | | performEvent { $0.request(request, didSuspendTask: task) } |
| | | } |
| | | |
| | | public func requestDidCancel(_ request: Request) { |
| | | performEvent { $0.requestDidCancel(request) } |
| | | } |
| | | |
| | | public func request(_ request: Request, didCancelTask task: URLSessionTask) { |
| | | performEvent { $0.request(request, didCancelTask: task) } |
| | | } |
| | | |
| | | public func request(_ request: DataRequest, |
| | | didValidateRequest urlRequest: URLRequest?, |
| | | response: HTTPURLResponse, |
| | | data: Data?, |
| | | withResult result: Request.ValidationResult) { |
| | | performEvent { $0.request(request, |
| | | didValidateRequest: urlRequest, |
| | | response: response, |
| | | data: data, |
| | | withResult: result) |
| | | } |
| | | } |
| | | |
| | | public func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>) { |
| | | performEvent { $0.request(request, didParseResponse: response) } |
| | | } |
| | | |
| | | public func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) { |
| | | performEvent { $0.request(request, didParseResponse: response) } |
| | | } |
| | | |
| | | public func request(_ request: DataStreamRequest, |
| | | didValidateRequest urlRequest: URLRequest?, |
| | | response: HTTPURLResponse, |
| | | withResult result: Request.ValidationResult) { |
| | | performEvent { $0.request(request, |
| | | didValidateRequest: urlRequest, |
| | | response: response, |
| | | withResult: result) |
| | | } |
| | | } |
| | | |
| | | public func request<Value>(_ request: DataStreamRequest, didParseStream result: Result<Value, AFError>) { |
| | | performEvent { $0.request(request, didParseStream: result) } |
| | | } |
| | | |
| | | public func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) { |
| | | performEvent { $0.request(request, didCreateUploadable: uploadable) } |
| | | } |
| | | |
| | | public func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) { |
| | | performEvent { $0.request(request, didFailToCreateUploadableWithError: error) } |
| | | } |
| | | |
| | | public func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) { |
| | | performEvent { $0.request(request, didProvideInputStream: stream) } |
| | | } |
| | | |
| | | public func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result<URL, AFError>) { |
| | | performEvent { $0.request(request, didFinishDownloadingUsing: task, with: result) } |
| | | } |
| | | |
| | | public func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) { |
| | | performEvent { $0.request(request, didCreateDestinationURL: url) } |
| | | } |
| | | |
| | | public func request(_ request: DownloadRequest, |
| | | didValidateRequest urlRequest: URLRequest?, |
| | | response: HTTPURLResponse, |
| | | fileURL: URL?, |
| | | withResult result: Request.ValidationResult) { |
| | | performEvent { $0.request(request, |
| | | didValidateRequest: urlRequest, |
| | | response: response, |
| | | fileURL: fileURL, |
| | | withResult: result) } |
| | | } |
| | | |
| | | public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse<URL?, AFError>) { |
| | | performEvent { $0.request(request, didParseResponse: response) } |
| | | } |
| | | |
| | | public func request<Value>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, AFError>) { |
| | | performEvent { $0.request(request, didParseResponse: response) } |
| | | } |
| | | } |
| | | |
| | | /// `EventMonitor` that allows optional closures to be set to receive events. |
| | | open class ClosureEventMonitor: EventMonitor { |
| | | /// Closure called on the `urlSession(_:didBecomeInvalidWithError:)` event. |
| | | open var sessionDidBecomeInvalidWithError: ((URLSession, Error?) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:task:didReceive:completionHandler:)`. |
| | | open var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> Void)? |
| | | |
| | | /// Closure that receives `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` event. |
| | | open var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:task:needNewBodyStream:)` event. |
| | | open var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)` event. |
| | | open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:task:didFinishCollecting:)` event. |
| | | open var taskDidFinishCollectingMetrics: ((URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:task:didCompleteWithError:)` event. |
| | | open var taskDidComplete: ((URLSession, URLSessionTask, Error?) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:taskIsWaitingForConnectivity:)` event. |
| | | open var taskIsWaitingForConnectivity: ((URLSession, URLSessionTask) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:dataTask:didReceive:completionHandler:)` event. |
| | | open var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> Void)? |
| | | |
| | | /// Closure that receives the `urlSession(_:dataTask:didReceive:)` event. |
| | | open var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:dataTask:willCacheResponse:completionHandler:)` event. |
| | | open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:downloadTask:didFinishDownloadingTo:)` event. |
| | | open var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)` |
| | | /// event. |
| | | open var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)` event. |
| | | open var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)? |
| | | |
| | | // MARK: - Request Events |
| | | |
| | | /// Closure called on the `request(_:didCreateInitialURLRequest:)` event. |
| | | open var requestDidCreateInitialURLRequest: ((Request, URLRequest) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didFailToCreateURLRequestWithError:)` event. |
| | | open var requestDidFailToCreateURLRequestWithError: ((Request, AFError) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didAdaptInitialRequest:to:)` event. |
| | | open var requestDidAdaptInitialRequestToAdaptedRequest: ((Request, URLRequest, URLRequest) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didFailToAdaptURLRequest:withError:)` event. |
| | | open var requestDidFailToAdaptURLRequestWithError: ((Request, URLRequest, AFError) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didCreateURLRequest:)` event. |
| | | open var requestDidCreateURLRequest: ((Request, URLRequest) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didCreateTask:)` event. |
| | | open var requestDidCreateTask: ((Request, URLSessionTask) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didGatherMetrics:)` event. |
| | | open var requestDidGatherMetrics: ((Request, URLSessionTaskMetrics) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didFailTask:earlyWithError:)` event. |
| | | open var requestDidFailTaskEarlyWithError: ((Request, URLSessionTask, AFError) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didCompleteTask:with:)` event. |
| | | open var requestDidCompleteTaskWithError: ((Request, URLSessionTask, AFError?) -> Void)? |
| | | |
| | | /// Closure called on the `requestIsRetrying(_:)` event. |
| | | open var requestIsRetrying: ((Request) -> Void)? |
| | | |
| | | /// Closure called on the `requestDidFinish(_:)` event. |
| | | open var requestDidFinish: ((Request) -> Void)? |
| | | |
| | | /// Closure called on the `requestDidResume(_:)` event. |
| | | open var requestDidResume: ((Request) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didResumeTask:)` event. |
| | | open var requestDidResumeTask: ((Request, URLSessionTask) -> Void)? |
| | | |
| | | /// Closure called on the `requestDidSuspend(_:)` event. |
| | | open var requestDidSuspend: ((Request) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didSuspendTask:)` event. |
| | | open var requestDidSuspendTask: ((Request, URLSessionTask) -> Void)? |
| | | |
| | | /// Closure called on the `requestDidCancel(_:)` event. |
| | | open var requestDidCancel: ((Request) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didCancelTask:)` event. |
| | | open var requestDidCancelTask: ((Request, URLSessionTask) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didValidateRequest:response:data:withResult:)` event. |
| | | open var requestDidValidateRequestResponseDataWithResult: ((DataRequest, URLRequest?, HTTPURLResponse, Data?, Request.ValidationResult) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didParseResponse:)` event. |
| | | open var requestDidParseResponse: ((DataRequest, DataResponse<Data?, AFError>) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didValidateRequest:response:withResult:)` event. |
| | | open var requestDidValidateRequestResponseWithResult: ((DataStreamRequest, URLRequest?, HTTPURLResponse, Request.ValidationResult) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didCreateUploadable:)` event. |
| | | open var requestDidCreateUploadable: ((UploadRequest, UploadRequest.Uploadable) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didFailToCreateUploadableWithError:)` event. |
| | | open var requestDidFailToCreateUploadableWithError: ((UploadRequest, AFError) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didProvideInputStream:)` event. |
| | | open var requestDidProvideInputStream: ((UploadRequest, InputStream) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didFinishDownloadingUsing:with:)` event. |
| | | open var requestDidFinishDownloadingUsingTaskWithResult: ((DownloadRequest, URLSessionTask, Result<URL, AFError>) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didCreateDestinationURL:)` event. |
| | | open var requestDidCreateDestinationURL: ((DownloadRequest, URL) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didValidateRequest:response:temporaryURL:destinationURL:withResult:)` event. |
| | | open var requestDidValidateRequestResponseFileURLWithResult: ((DownloadRequest, URLRequest?, HTTPURLResponse, URL?, Request.ValidationResult) -> Void)? |
| | | |
| | | /// Closure called on the `request(_:didParseResponse:)` event. |
| | | open var requestDidParseDownloadResponse: ((DownloadRequest, DownloadResponse<URL?, AFError>) -> Void)? |
| | | |
| | | public let queue: DispatchQueue |
| | | |
| | | /// Creates an instance using the provided queue. |
| | | /// |
| | | /// - Parameter queue: `DispatchQueue` on which events will fired. `.main` by default. |
| | | public init(queue: DispatchQueue = .main) { |
| | | self.queue = queue |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { |
| | | sessionDidBecomeInvalidWithError?(session, error) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) { |
| | | taskDidReceiveChallenge?(session, task, challenge) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, |
| | | task: URLSessionTask, |
| | | didSendBodyData bytesSent: Int64, |
| | | totalBytesSent: Int64, |
| | | totalBytesExpectedToSend: Int64) { |
| | | taskDidSendBodyData?(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) { |
| | | taskNeedNewBodyStream?(session, task) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, |
| | | task: URLSessionTask, |
| | | willPerformHTTPRedirection response: HTTPURLResponse, |
| | | newRequest request: URLRequest) { |
| | | taskWillPerformHTTPRedirection?(session, task, response, request) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { |
| | | taskDidFinishCollectingMetrics?(session, task, metrics) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { |
| | | taskDidComplete?(session, task, error) |
| | | } |
| | | |
| | | @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) |
| | | open func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { |
| | | taskIsWaitingForConnectivity?(session, task) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) { |
| | | dataTaskDidReceiveResponse?(session, dataTask, response) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { |
| | | dataTaskDidReceiveData?(session, dataTask, data) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse) { |
| | | dataTaskWillCacheResponse?(session, dataTask, proposedResponse) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, |
| | | downloadTask: URLSessionDownloadTask, |
| | | didResumeAtOffset fileOffset: Int64, |
| | | expectedTotalBytes: Int64) { |
| | | downloadTaskDidResumeAtOffset?(session, downloadTask, fileOffset, expectedTotalBytes) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, |
| | | downloadTask: URLSessionDownloadTask, |
| | | didWriteData bytesWritten: Int64, |
| | | totalBytesWritten: Int64, |
| | | totalBytesExpectedToWrite: Int64) { |
| | | downloadTaskDidWriteData?(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { |
| | | downloadTaskDidFinishDownloadingToURL?(session, downloadTask, location) |
| | | } |
| | | |
| | | // MARK: Request Events |
| | | |
| | | open func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) { |
| | | requestDidCreateInitialURLRequest?(request, urlRequest) |
| | | } |
| | | |
| | | open func request(_ request: Request, didFailToCreateURLRequestWithError error: AFError) { |
| | | requestDidFailToCreateURLRequestWithError?(request, error) |
| | | } |
| | | |
| | | open func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) { |
| | | requestDidAdaptInitialRequestToAdaptedRequest?(request, initialRequest, adaptedRequest) |
| | | } |
| | | |
| | | open func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: AFError) { |
| | | requestDidFailToAdaptURLRequestWithError?(request, initialRequest, error) |
| | | } |
| | | |
| | | open func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) { |
| | | requestDidCreateURLRequest?(request, urlRequest) |
| | | } |
| | | |
| | | open func request(_ request: Request, didCreateTask task: URLSessionTask) { |
| | | requestDidCreateTask?(request, task) |
| | | } |
| | | |
| | | open func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) { |
| | | requestDidGatherMetrics?(request, metrics) |
| | | } |
| | | |
| | | open func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: AFError) { |
| | | requestDidFailTaskEarlyWithError?(request, task, error) |
| | | } |
| | | |
| | | open func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) { |
| | | requestDidCompleteTaskWithError?(request, task, error) |
| | | } |
| | | |
| | | open func requestIsRetrying(_ request: Request) { |
| | | requestIsRetrying?(request) |
| | | } |
| | | |
| | | open func requestDidFinish(_ request: Request) { |
| | | requestDidFinish?(request) |
| | | } |
| | | |
| | | open func requestDidResume(_ request: Request) { |
| | | requestDidResume?(request) |
| | | } |
| | | |
| | | public func request(_ request: Request, didResumeTask task: URLSessionTask) { |
| | | requestDidResumeTask?(request, task) |
| | | } |
| | | |
| | | open func requestDidSuspend(_ request: Request) { |
| | | requestDidSuspend?(request) |
| | | } |
| | | |
| | | public func request(_ request: Request, didSuspendTask task: URLSessionTask) { |
| | | requestDidSuspendTask?(request, task) |
| | | } |
| | | |
| | | open func requestDidCancel(_ request: Request) { |
| | | requestDidCancel?(request) |
| | | } |
| | | |
| | | public func request(_ request: Request, didCancelTask task: URLSessionTask) { |
| | | requestDidCancelTask?(request, task) |
| | | } |
| | | |
| | | open func request(_ request: DataRequest, |
| | | didValidateRequest urlRequest: URLRequest?, |
| | | response: HTTPURLResponse, |
| | | data: Data?, |
| | | withResult result: Request.ValidationResult) { |
| | | requestDidValidateRequestResponseDataWithResult?(request, urlRequest, response, data, result) |
| | | } |
| | | |
| | | open func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>) { |
| | | requestDidParseResponse?(request, response) |
| | | } |
| | | |
| | | public func request(_ request: DataStreamRequest, didValidateRequest urlRequest: URLRequest?, response: HTTPURLResponse, withResult result: Request.ValidationResult) { |
| | | requestDidValidateRequestResponseWithResult?(request, urlRequest, response, result) |
| | | } |
| | | |
| | | open func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) { |
| | | requestDidCreateUploadable?(request, uploadable) |
| | | } |
| | | |
| | | open func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) { |
| | | requestDidFailToCreateUploadableWithError?(request, error) |
| | | } |
| | | |
| | | open func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) { |
| | | requestDidProvideInputStream?(request, stream) |
| | | } |
| | | |
| | | open func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result<URL, AFError>) { |
| | | requestDidFinishDownloadingUsingTaskWithResult?(request, task, result) |
| | | } |
| | | |
| | | open func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) { |
| | | requestDidCreateDestinationURL?(request, url) |
| | | } |
| | | |
| | | open func request(_ request: DownloadRequest, |
| | | didValidateRequest urlRequest: URLRequest?, |
| | | response: HTTPURLResponse, |
| | | fileURL: URL?, |
| | | withResult result: Request.ValidationResult) { |
| | | requestDidValidateRequestResponseFileURLWithResult?(request, |
| | | urlRequest, |
| | | response, |
| | | fileURL, |
| | | result) |
| | | } |
| | | |
| | | open func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse<URL?, AFError>) { |
| | | requestDidParseDownloadResponse?(request, response) |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // HTTPHeaders.swift |
| | | // |
| | | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | /// An order-preserving and case-insensitive representation of HTTP headers. |
| | | public struct HTTPHeaders { |
| | | private var headers: [HTTPHeader] = [] |
| | | |
| | | /// Creates an empty instance. |
| | | public init() {} |
| | | |
| | | /// Creates an instance from an array of `HTTPHeader`s. Duplicate case-insensitive names are collapsed into the last |
| | | /// name and value encountered. |
| | | public init(_ headers: [HTTPHeader]) { |
| | | headers.forEach { update($0) } |
| | | } |
| | | |
| | | /// Creates an instance from a `[String: String]`. Duplicate case-insensitive names are collapsed into the last name |
| | | /// and value encountered. |
| | | public init(_ dictionary: [String: String]) { |
| | | dictionary.forEach { update(HTTPHeader(name: $0.key, value: $0.value)) } |
| | | } |
| | | |
| | | /// Case-insensitively updates or appends an `HTTPHeader` into the instance using the provided `name` and `value`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - name: The `HTTPHeader` name. |
| | | /// - value: The `HTTPHeader value. |
| | | public mutating func add(name: String, value: String) { |
| | | update(HTTPHeader(name: name, value: value)) |
| | | } |
| | | |
| | | /// Case-insensitively updates or appends the provided `HTTPHeader` into the instance. |
| | | /// |
| | | /// - Parameter header: The `HTTPHeader` to update or append. |
| | | public mutating func add(_ header: HTTPHeader) { |
| | | update(header) |
| | | } |
| | | |
| | | /// Case-insensitively updates or appends an `HTTPHeader` into the instance using the provided `name` and `value`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - name: The `HTTPHeader` name. |
| | | /// - value: The `HTTPHeader value. |
| | | public mutating func update(name: String, value: String) { |
| | | update(HTTPHeader(name: name, value: value)) |
| | | } |
| | | |
| | | /// Case-insensitively updates or appends the provided `HTTPHeader` into the instance. |
| | | /// |
| | | /// - Parameter header: The `HTTPHeader` to update or append. |
| | | public mutating func update(_ header: HTTPHeader) { |
| | | guard let index = headers.index(of: header.name) else { |
| | | headers.append(header) |
| | | return |
| | | } |
| | | |
| | | headers.replaceSubrange(index...index, with: [header]) |
| | | } |
| | | |
| | | /// Case-insensitively removes an `HTTPHeader`, if it exists, from the instance. |
| | | /// |
| | | /// - Parameter name: The name of the `HTTPHeader` to remove. |
| | | public mutating func remove(name: String) { |
| | | guard let index = headers.index(of: name) else { return } |
| | | |
| | | headers.remove(at: index) |
| | | } |
| | | |
| | | /// Sort the current instance by header name, case insensitively. |
| | | public mutating func sort() { |
| | | headers.sort { $0.name.lowercased() < $1.name.lowercased() } |
| | | } |
| | | |
| | | /// Returns an instance sorted by header name. |
| | | /// |
| | | /// - Returns: A copy of the current instance sorted by name. |
| | | public func sorted() -> HTTPHeaders { |
| | | var headers = self |
| | | headers.sort() |
| | | |
| | | return headers |
| | | } |
| | | |
| | | /// Case-insensitively find a header's value by name. |
| | | /// |
| | | /// - Parameter name: The name of the header to search for, case-insensitively. |
| | | /// |
| | | /// - Returns: The value of header, if it exists. |
| | | public func value(for name: String) -> String? { |
| | | guard let index = headers.index(of: name) else { return nil } |
| | | |
| | | return headers[index].value |
| | | } |
| | | |
| | | /// Case-insensitively access the header with the given name. |
| | | /// |
| | | /// - Parameter name: The name of the header. |
| | | public subscript(_ name: String) -> String? { |
| | | get { value(for: name) } |
| | | set { |
| | | if let value = newValue { |
| | | update(name: name, value: value) |
| | | } else { |
| | | remove(name: name) |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// The dictionary representation of all headers. |
| | | /// |
| | | /// This representation does not preserve the current order of the instance. |
| | | public var dictionary: [String: String] { |
| | | let namesAndValues = headers.map { ($0.name, $0.value) } |
| | | |
| | | return Dictionary(namesAndValues, uniquingKeysWith: { _, last in last }) |
| | | } |
| | | } |
| | | |
| | | extension HTTPHeaders: ExpressibleByDictionaryLiteral { |
| | | public init(dictionaryLiteral elements: (String, String)...) { |
| | | elements.forEach { update(name: $0.0, value: $0.1) } |
| | | } |
| | | } |
| | | |
| | | extension HTTPHeaders: ExpressibleByArrayLiteral { |
| | | public init(arrayLiteral elements: HTTPHeader...) { |
| | | self.init(elements) |
| | | } |
| | | } |
| | | |
| | | extension HTTPHeaders: Sequence { |
| | | public func makeIterator() -> IndexingIterator<[HTTPHeader]> { |
| | | headers.makeIterator() |
| | | } |
| | | } |
| | | |
| | | extension HTTPHeaders: Collection { |
| | | public var startIndex: Int { |
| | | headers.startIndex |
| | | } |
| | | |
| | | public var endIndex: Int { |
| | | headers.endIndex |
| | | } |
| | | |
| | | public subscript(position: Int) -> HTTPHeader { |
| | | headers[position] |
| | | } |
| | | |
| | | public func index(after i: Int) -> Int { |
| | | headers.index(after: i) |
| | | } |
| | | } |
| | | |
| | | extension HTTPHeaders: CustomStringConvertible { |
| | | public var description: String { |
| | | headers.map(\.description) |
| | | .joined(separator: "\n") |
| | | } |
| | | } |
| | | |
| | | // MARK: - HTTPHeader |
| | | |
| | | /// A representation of a single HTTP header's name / value pair. |
| | | public struct HTTPHeader: Hashable { |
| | | /// Name of the header. |
| | | public let name: String |
| | | |
| | | /// Value of the header. |
| | | public let value: String |
| | | |
| | | /// Creates an instance from the given `name` and `value`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - name: The name of the header. |
| | | /// - value: The value of the header. |
| | | public init(name: String, value: String) { |
| | | self.name = name |
| | | self.value = value |
| | | } |
| | | } |
| | | |
| | | extension HTTPHeader: CustomStringConvertible { |
| | | public var description: String { |
| | | "\(name): \(value)" |
| | | } |
| | | } |
| | | |
| | | extension HTTPHeader { |
| | | /// Returns an `Accept` header. |
| | | /// |
| | | /// - Parameter value: The `Accept` value. |
| | | /// - Returns: The header. |
| | | public static func accept(_ value: String) -> HTTPHeader { |
| | | HTTPHeader(name: "Accept", value: value) |
| | | } |
| | | |
| | | /// Returns an `Accept-Charset` header. |
| | | /// |
| | | /// - Parameter value: The `Accept-Charset` value. |
| | | /// - Returns: The header. |
| | | public static func acceptCharset(_ value: String) -> HTTPHeader { |
| | | HTTPHeader(name: "Accept-Charset", value: value) |
| | | } |
| | | |
| | | /// Returns an `Accept-Language` header. |
| | | /// |
| | | /// Alamofire offers a default Accept-Language header that accumulates and encodes the system's preferred languages. |
| | | /// Use `HTTPHeader.defaultAcceptLanguage`. |
| | | /// |
| | | /// - Parameter value: The `Accept-Language` value. |
| | | /// |
| | | /// - Returns: The header. |
| | | public static func acceptLanguage(_ value: String) -> HTTPHeader { |
| | | HTTPHeader(name: "Accept-Language", value: value) |
| | | } |
| | | |
| | | /// Returns an `Accept-Encoding` header. |
| | | /// |
| | | /// Alamofire offers a default accept encoding value that provides the most common values. Use |
| | | /// `HTTPHeader.defaultAcceptEncoding`. |
| | | /// |
| | | /// - Parameter value: The `Accept-Encoding` value. |
| | | /// |
| | | /// - Returns: The header |
| | | public static func acceptEncoding(_ value: String) -> HTTPHeader { |
| | | HTTPHeader(name: "Accept-Encoding", value: value) |
| | | } |
| | | |
| | | /// Returns a `Basic` `Authorization` header using the `username` and `password` provided. |
| | | /// |
| | | /// - Parameters: |
| | | /// - username: The username of the header. |
| | | /// - password: The password of the header. |
| | | /// |
| | | /// - Returns: The header. |
| | | public static func authorization(username: String, password: String) -> HTTPHeader { |
| | | let credential = Data("\(username):\(password)".utf8).base64EncodedString() |
| | | |
| | | return authorization("Basic \(credential)") |
| | | } |
| | | |
| | | /// Returns a `Bearer` `Authorization` header using the `bearerToken` provided |
| | | /// |
| | | /// - Parameter bearerToken: The bearer token. |
| | | /// |
| | | /// - Returns: The header. |
| | | public static func authorization(bearerToken: String) -> HTTPHeader { |
| | | authorization("Bearer \(bearerToken)") |
| | | } |
| | | |
| | | /// Returns an `Authorization` header. |
| | | /// |
| | | /// Alamofire provides built-in methods to produce `Authorization` headers. For a Basic `Authorization` header use |
| | | /// `HTTPHeader.authorization(username:password:)`. For a Bearer `Authorization` header, use |
| | | /// `HTTPHeader.authorization(bearerToken:)`. |
| | | /// |
| | | /// - Parameter value: The `Authorization` value. |
| | | /// |
| | | /// - Returns: The header. |
| | | public static func authorization(_ value: String) -> HTTPHeader { |
| | | HTTPHeader(name: "Authorization", value: value) |
| | | } |
| | | |
| | | /// Returns a `Content-Disposition` header. |
| | | /// |
| | | /// - Parameter value: The `Content-Disposition` value. |
| | | /// |
| | | /// - Returns: The header. |
| | | public static func contentDisposition(_ value: String) -> HTTPHeader { |
| | | HTTPHeader(name: "Content-Disposition", value: value) |
| | | } |
| | | |
| | | /// Returns a `Content-Encoding` header. |
| | | /// |
| | | /// - Parameter value: The `Content-Encoding`. |
| | | /// |
| | | /// - Returns: The header. |
| | | public static func contentEncoding(_ value: String) -> HTTPHeader { |
| | | HTTPHeader(name: "Content-Encoding", value: value) |
| | | } |
| | | |
| | | /// Returns a `Content-Type` header. |
| | | /// |
| | | /// All Alamofire `ParameterEncoding`s and `ParameterEncoder`s set the `Content-Type` of the request, so it may not |
| | | /// be necessary to manually set this value. |
| | | /// |
| | | /// - Parameter value: The `Content-Type` value. |
| | | /// |
| | | /// - Returns: The header. |
| | | public static func contentType(_ value: String) -> HTTPHeader { |
| | | HTTPHeader(name: "Content-Type", value: value) |
| | | } |
| | | |
| | | /// Returns a `User-Agent` header. |
| | | /// |
| | | /// - Parameter value: The `User-Agent` value. |
| | | /// |
| | | /// - Returns: The header. |
| | | public static func userAgent(_ value: String) -> HTTPHeader { |
| | | HTTPHeader(name: "User-Agent", value: value) |
| | | } |
| | | } |
| | | |
| | | extension Array where Element == HTTPHeader { |
| | | /// Case-insensitively finds the index of an `HTTPHeader` with the provided name, if it exists. |
| | | func index(of name: String) -> Int? { |
| | | let lowercasedName = name.lowercased() |
| | | return firstIndex { $0.name.lowercased() == lowercasedName } |
| | | } |
| | | } |
| | | |
| | | // MARK: - Defaults |
| | | |
| | | extension HTTPHeaders { |
| | | /// The default set of `HTTPHeaders` used by Alamofire. Includes `Accept-Encoding`, `Accept-Language`, and |
| | | /// `User-Agent`. |
| | | public static let `default`: HTTPHeaders = [.defaultAcceptEncoding, |
| | | .defaultAcceptLanguage, |
| | | .defaultUserAgent] |
| | | } |
| | | |
| | | extension HTTPHeader { |
| | | /// Returns Alamofire's default `Accept-Encoding` header, appropriate for the encodings supported by particular OS |
| | | /// versions. |
| | | /// |
| | | /// See the [Accept-Encoding HTTP header documentation](https://tools.ietf.org/html/rfc7230#section-4.2.3) . |
| | | public static let defaultAcceptEncoding: HTTPHeader = { |
| | | let encodings: [String] |
| | | if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) { |
| | | encodings = ["br", "gzip", "deflate"] |
| | | } else { |
| | | encodings = ["gzip", "deflate"] |
| | | } |
| | | |
| | | return .acceptEncoding(encodings.qualityEncoded()) |
| | | }() |
| | | |
| | | /// Returns Alamofire's default `Accept-Language` header, generated by querying `Locale` for the user's |
| | | /// `preferredLanguages`. |
| | | /// |
| | | /// See the [Accept-Language HTTP header documentation](https://tools.ietf.org/html/rfc7231#section-5.3.5). |
| | | public static let defaultAcceptLanguage: HTTPHeader = .acceptLanguage(Locale.preferredLanguages.prefix(6).qualityEncoded()) |
| | | |
| | | /// Returns Alamofire's default `User-Agent` header. |
| | | /// |
| | | /// See the [User-Agent header documentation](https://tools.ietf.org/html/rfc7231#section-5.5.3). |
| | | /// |
| | | /// Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 13.0.0) Alamofire/5.0.0` |
| | | public static let defaultUserAgent: HTTPHeader = { |
| | | let info = Bundle.main.infoDictionary |
| | | let executable = (info?["CFBundleExecutable"] as? String) ?? |
| | | (ProcessInfo.processInfo.arguments.first?.split(separator: "/").last.map(String.init)) ?? |
| | | "Unknown" |
| | | let bundle = info?["CFBundleIdentifier"] as? String ?? "Unknown" |
| | | let appVersion = info?["CFBundleShortVersionString"] as? String ?? "Unknown" |
| | | let appBuild = info?["CFBundleVersion"] as? String ?? "Unknown" |
| | | |
| | | let osNameVersion: String = { |
| | | let version = ProcessInfo.processInfo.operatingSystemVersion |
| | | let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" |
| | | let osName: String = { |
| | | #if os(iOS) |
| | | #if targetEnvironment(macCatalyst) |
| | | return "macOS(Catalyst)" |
| | | #else |
| | | return "iOS" |
| | | #endif |
| | | #elseif os(watchOS) |
| | | return "watchOS" |
| | | #elseif os(tvOS) |
| | | return "tvOS" |
| | | #elseif os(macOS) |
| | | return "macOS" |
| | | #elseif os(Linux) |
| | | return "Linux" |
| | | #elseif os(Windows) |
| | | return "Windows" |
| | | #elseif os(Android) |
| | | return "Android" |
| | | #else |
| | | return "Unknown" |
| | | #endif |
| | | }() |
| | | |
| | | return "\(osName) \(versionString)" |
| | | }() |
| | | |
| | | let alamofireVersion = "Alamofire/\(version)" |
| | | |
| | | let userAgent = "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)" |
| | | |
| | | return .userAgent(userAgent) |
| | | }() |
| | | } |
| | | |
| | | extension Collection where Element == String { |
| | | func qualityEncoded() -> String { |
| | | enumerated().map { index, encoding in |
| | | let quality = 1.0 - (Double(index) * 0.1) |
| | | return "\(encoding);q=\(quality)" |
| | | }.joined(separator: ", ") |
| | | } |
| | | } |
| | | |
| | | // MARK: - System Type Extensions |
| | | |
| | | extension URLRequest { |
| | | /// Returns `allHTTPHeaderFields` as `HTTPHeaders`. |
| | | public var headers: HTTPHeaders { |
| | | get { allHTTPHeaderFields.map(HTTPHeaders.init) ?? HTTPHeaders() } |
| | | set { allHTTPHeaderFields = newValue.dictionary } |
| | | } |
| | | } |
| | | |
| | | extension HTTPURLResponse { |
| | | /// Returns `allHeaderFields` as `HTTPHeaders`. |
| | | public var headers: HTTPHeaders { |
| | | (allHeaderFields as? [String: String]).map(HTTPHeaders.init) ?? HTTPHeaders() |
| | | } |
| | | } |
| | | |
| | | extension URLSessionConfiguration { |
| | | /// Returns `httpAdditionalHeaders` as `HTTPHeaders`. |
| | | public var headers: HTTPHeaders { |
| | | get { (httpAdditionalHeaders as? [String: String]).map(HTTPHeaders.init) ?? HTTPHeaders() } |
| | | set { httpAdditionalHeaders = newValue.dictionary } |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // HTTPMethod.swift |
| | | // |
| | | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | /// Type representing HTTP methods. Raw `String` value is stored and compared case-sensitively, so |
| | | /// `HTTPMethod.get != HTTPMethod(rawValue: "get")`. |
| | | /// |
| | | /// See https://tools.ietf.org/html/rfc7231#section-4.3 |
| | | public struct HTTPMethod: RawRepresentable, Equatable, Hashable { |
| | | /// `CONNECT` method. |
| | | public static let connect = HTTPMethod(rawValue: "CONNECT") |
| | | /// `DELETE` method. |
| | | public static let delete = HTTPMethod(rawValue: "DELETE") |
| | | /// `GET` method. |
| | | public static let get = HTTPMethod(rawValue: "GET") |
| | | /// `HEAD` method. |
| | | public static let head = HTTPMethod(rawValue: "HEAD") |
| | | /// `OPTIONS` method. |
| | | public static let options = HTTPMethod(rawValue: "OPTIONS") |
| | | /// `PATCH` method. |
| | | public static let patch = HTTPMethod(rawValue: "PATCH") |
| | | /// `POST` method. |
| | | public static let post = HTTPMethod(rawValue: "POST") |
| | | /// `PUT` method. |
| | | public static let put = HTTPMethod(rawValue: "PUT") |
| | | /// `QUERY` method. |
| | | public static let query = HTTPMethod(rawValue: "QUERY") |
| | | /// `TRACE` method. |
| | | public static let trace = HTTPMethod(rawValue: "TRACE") |
| | | |
| | | public let rawValue: String |
| | | |
| | | public init(rawValue: String) { |
| | | self.rawValue = rawValue |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // MultipartFormData.swift |
| | | // |
| | | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | #if canImport(MobileCoreServices) |
| | | import MobileCoreServices |
| | | #elseif canImport(CoreServices) |
| | | import CoreServices |
| | | #endif |
| | | |
| | | /// Constructs `multipart/form-data` for uploads within an HTTP or HTTPS body. There are currently two ways to encode |
| | | /// multipart form data. The first way is to encode the data directly in memory. This is very efficient, but can lead |
| | | /// to memory issues if the dataset is too large. The second way is designed for larger datasets and will write all the |
| | | /// data to a single file on disk with all the proper boundary segmentation. The second approach MUST be used for |
| | | /// larger datasets such as video content, otherwise your app may run out of memory when trying to encode the dataset. |
| | | /// |
| | | /// For more information on `multipart/form-data` in general, please refer to the RFC-2388 and RFC-2045 specs as well |
| | | /// and the w3 form documentation. |
| | | /// |
| | | /// - https://www.ietf.org/rfc/rfc2388.txt |
| | | /// - https://www.ietf.org/rfc/rfc2045.txt |
| | | /// - https://www.w3.org/TR/html401/interact/forms.html#h-17.13 |
| | | open class MultipartFormData { |
| | | // MARK: - Helper Types |
| | | |
| | | enum EncodingCharacters { |
| | | static let crlf = "\r\n" |
| | | } |
| | | |
| | | enum BoundaryGenerator { |
| | | enum BoundaryType { |
| | | case initial, encapsulated, final |
| | | } |
| | | |
| | | static func randomBoundary() -> String { |
| | | let first = UInt32.random(in: UInt32.min...UInt32.max) |
| | | let second = UInt32.random(in: UInt32.min...UInt32.max) |
| | | |
| | | return String(format: "alamofire.boundary.%08x%08x", first, second) |
| | | } |
| | | |
| | | static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data { |
| | | let boundaryText: String |
| | | |
| | | switch boundaryType { |
| | | case .initial: |
| | | boundaryText = "--\(boundary)\(EncodingCharacters.crlf)" |
| | | case .encapsulated: |
| | | boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)" |
| | | case .final: |
| | | boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)" |
| | | } |
| | | |
| | | return Data(boundaryText.utf8) |
| | | } |
| | | } |
| | | |
| | | class BodyPart { |
| | | let headers: HTTPHeaders |
| | | let bodyStream: InputStream |
| | | let bodyContentLength: UInt64 |
| | | var hasInitialBoundary = false |
| | | var hasFinalBoundary = false |
| | | |
| | | init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64) { |
| | | self.headers = headers |
| | | self.bodyStream = bodyStream |
| | | self.bodyContentLength = bodyContentLength |
| | | } |
| | | } |
| | | |
| | | // MARK: - Properties |
| | | |
| | | /// Default memory threshold used when encoding `MultipartFormData`, in bytes. |
| | | public static let encodingMemoryThreshold: UInt64 = 10_000_000 |
| | | |
| | | /// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`. |
| | | open lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)" |
| | | |
| | | /// The content length of all body parts used to generate the `multipart/form-data` not including the boundaries. |
| | | public var contentLength: UInt64 { bodyParts.reduce(0) { $0 + $1.bodyContentLength } } |
| | | |
| | | /// The boundary used to separate the body parts in the encoded form data. |
| | | public let boundary: String |
| | | |
| | | let fileManager: FileManager |
| | | |
| | | private var bodyParts: [BodyPart] |
| | | private var bodyPartError: AFError? |
| | | private let streamBufferSize: Int |
| | | |
| | | // MARK: - Lifecycle |
| | | |
| | | /// Creates an instance. |
| | | /// |
| | | /// - Parameters: |
| | | /// - fileManager: `FileManager` to use for file operations, if needed. |
| | | /// - boundary: Boundary `String` used to separate body parts. |
| | | public init(fileManager: FileManager = .default, boundary: String? = nil) { |
| | | self.fileManager = fileManager |
| | | self.boundary = boundary ?? BoundaryGenerator.randomBoundary() |
| | | bodyParts = [] |
| | | |
| | | // |
| | | // The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more |
| | | // information, please refer to the following article: |
| | | // - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html |
| | | // |
| | | streamBufferSize = 1024 |
| | | } |
| | | |
| | | // MARK: - Body Parts |
| | | |
| | | /// Creates a body part from the data and appends it to the instance. |
| | | /// |
| | | /// The body part data will be encoded using the following format: |
| | | /// |
| | | /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header) |
| | | /// - `Content-Type: #{mimeType}` (HTTP Header) |
| | | /// - Encoded file data |
| | | /// - Multipart form boundary |
| | | /// |
| | | /// - Parameters: |
| | | /// - data: `Data` to encoding into the instance. |
| | | /// - name: Name to associate with the `Data` in the `Content-Disposition` HTTP header. |
| | | /// - fileName: Filename to associate with the `Data` in the `Content-Disposition` HTTP header. |
| | | /// - mimeType: MIME type to associate with the data in the `Content-Type` HTTP header. |
| | | public func append(_ data: Data, withName name: String, fileName: String? = nil, mimeType: String? = nil) { |
| | | let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) |
| | | let stream = InputStream(data: data) |
| | | let length = UInt64(data.count) |
| | | |
| | | append(stream, withLength: length, headers: headers) |
| | | } |
| | | |
| | | /// Creates a body part from the file and appends it to the instance. |
| | | /// |
| | | /// The body part data will be encoded using the following format: |
| | | /// |
| | | /// - `Content-Disposition: form-data; name=#{name}; filename=#{generated filename}` (HTTP Header) |
| | | /// - `Content-Type: #{generated mimeType}` (HTTP Header) |
| | | /// - Encoded file data |
| | | /// - Multipart form boundary |
| | | /// |
| | | /// The filename in the `Content-Disposition` HTTP header is generated from the last path component of the |
| | | /// `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the |
| | | /// system associated MIME type. |
| | | /// |
| | | /// - Parameters: |
| | | /// - fileURL: `URL` of the file whose content will be encoded into the instance. |
| | | /// - name: Name to associate with the file content in the `Content-Disposition` HTTP header. |
| | | public func append(_ fileURL: URL, withName name: String) { |
| | | let fileName = fileURL.lastPathComponent |
| | | let pathExtension = fileURL.pathExtension |
| | | |
| | | if !fileName.isEmpty && !pathExtension.isEmpty { |
| | | let mime = mimeType(forPathExtension: pathExtension) |
| | | append(fileURL, withName: name, fileName: fileName, mimeType: mime) |
| | | } else { |
| | | setBodyPartError(withReason: .bodyPartFilenameInvalid(in: fileURL)) |
| | | } |
| | | } |
| | | |
| | | /// Creates a body part from the file and appends it to the instance. |
| | | /// |
| | | /// The body part data will be encoded using the following format: |
| | | /// |
| | | /// - Content-Disposition: form-data; name=#{name}; filename=#{filename} (HTTP Header) |
| | | /// - Content-Type: #{mimeType} (HTTP Header) |
| | | /// - Encoded file data |
| | | /// - Multipart form boundary |
| | | /// |
| | | /// - Parameters: |
| | | /// - fileURL: `URL` of the file whose content will be encoded into the instance. |
| | | /// - name: Name to associate with the file content in the `Content-Disposition` HTTP header. |
| | | /// - fileName: Filename to associate with the file content in the `Content-Disposition` HTTP header. |
| | | /// - mimeType: MIME type to associate with the file content in the `Content-Type` HTTP header. |
| | | public func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String) { |
| | | let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) |
| | | |
| | | //============================================================ |
| | | // Check 1 - is file URL? |
| | | //============================================================ |
| | | |
| | | guard fileURL.isFileURL else { |
| | | setBodyPartError(withReason: .bodyPartURLInvalid(url: fileURL)) |
| | | return |
| | | } |
| | | |
| | | //============================================================ |
| | | // Check 2 - is file URL reachable? |
| | | //============================================================ |
| | | |
| | | #if !(os(Linux) || os(Windows) || os(Android)) |
| | | do { |
| | | let isReachable = try fileURL.checkPromisedItemIsReachable() |
| | | guard isReachable else { |
| | | setBodyPartError(withReason: .bodyPartFileNotReachable(at: fileURL)) |
| | | return |
| | | } |
| | | } catch { |
| | | setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error)) |
| | | return |
| | | } |
| | | #endif |
| | | |
| | | //============================================================ |
| | | // Check 3 - is file URL a directory? |
| | | //============================================================ |
| | | |
| | | var isDirectory: ObjCBool = false |
| | | let path = fileURL.path |
| | | |
| | | guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else { |
| | | setBodyPartError(withReason: .bodyPartFileIsDirectory(at: fileURL)) |
| | | return |
| | | } |
| | | |
| | | //============================================================ |
| | | // Check 4 - can the file size be extracted? |
| | | //============================================================ |
| | | |
| | | let bodyContentLength: UInt64 |
| | | |
| | | do { |
| | | guard let fileSize = try fileManager.attributesOfItem(atPath: path)[.size] as? NSNumber else { |
| | | setBodyPartError(withReason: .bodyPartFileSizeNotAvailable(at: fileURL)) |
| | | return |
| | | } |
| | | |
| | | bodyContentLength = fileSize.uint64Value |
| | | } catch { |
| | | setBodyPartError(withReason: .bodyPartFileSizeQueryFailedWithError(forURL: fileURL, error: error)) |
| | | return |
| | | } |
| | | |
| | | //============================================================ |
| | | // Check 5 - can a stream be created from file URL? |
| | | //============================================================ |
| | | |
| | | guard let stream = InputStream(url: fileURL) else { |
| | | setBodyPartError(withReason: .bodyPartInputStreamCreationFailed(for: fileURL)) |
| | | return |
| | | } |
| | | |
| | | append(stream, withLength: bodyContentLength, headers: headers) |
| | | } |
| | | |
| | | /// Creates a body part from the stream and appends it to the instance. |
| | | /// |
| | | /// The body part data will be encoded using the following format: |
| | | /// |
| | | /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header) |
| | | /// - `Content-Type: #{mimeType}` (HTTP Header) |
| | | /// - Encoded stream data |
| | | /// - Multipart form boundary |
| | | /// |
| | | /// - Parameters: |
| | | /// - stream: `InputStream` to encode into the instance. |
| | | /// - length: Length, in bytes, of the stream. |
| | | /// - name: Name to associate with the stream content in the `Content-Disposition` HTTP header. |
| | | /// - fileName: Filename to associate with the stream content in the `Content-Disposition` HTTP header. |
| | | /// - mimeType: MIME type to associate with the stream content in the `Content-Type` HTTP header. |
| | | public func append(_ stream: InputStream, |
| | | withLength length: UInt64, |
| | | name: String, |
| | | fileName: String, |
| | | mimeType: String) { |
| | | let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) |
| | | append(stream, withLength: length, headers: headers) |
| | | } |
| | | |
| | | /// Creates a body part with the stream, length, and headers and appends it to the instance. |
| | | /// |
| | | /// The body part data will be encoded using the following format: |
| | | /// |
| | | /// - HTTP headers |
| | | /// - Encoded stream data |
| | | /// - Multipart form boundary |
| | | /// |
| | | /// - Parameters: |
| | | /// - stream: `InputStream` to encode into the instance. |
| | | /// - length: Length, in bytes, of the stream. |
| | | /// - headers: `HTTPHeaders` for the body part. |
| | | public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders) { |
| | | let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length) |
| | | bodyParts.append(bodyPart) |
| | | } |
| | | |
| | | // MARK: - Data Encoding |
| | | |
| | | /// Encodes all appended body parts into a single `Data` value. |
| | | /// |
| | | /// - Note: This method will load all the appended body parts into memory all at the same time. This method should |
| | | /// only be used when the encoded data will have a small memory footprint. For large data cases, please use |
| | | /// the `writeEncodedData(to:))` method. |
| | | /// |
| | | /// - Returns: The encoded `Data`, if encoding is successful. |
| | | /// - Throws: An `AFError` if encoding encounters an error. |
| | | public func encode() throws -> Data { |
| | | if let bodyPartError = bodyPartError { |
| | | throw bodyPartError |
| | | } |
| | | |
| | | var encoded = Data() |
| | | |
| | | bodyParts.first?.hasInitialBoundary = true |
| | | bodyParts.last?.hasFinalBoundary = true |
| | | |
| | | for bodyPart in bodyParts { |
| | | let encodedData = try encode(bodyPart) |
| | | encoded.append(encodedData) |
| | | } |
| | | |
| | | return encoded |
| | | } |
| | | |
| | | /// Writes all appended body parts to the given file `URL`. |
| | | /// |
| | | /// This process is facilitated by reading and writing with input and output streams, respectively. Thus, |
| | | /// this approach is very memory efficient and should be used for large body part data. |
| | | /// |
| | | /// - Parameter fileURL: File `URL` to which to write the form data. |
| | | /// - Throws: An `AFError` if encoding encounters an error. |
| | | public func writeEncodedData(to fileURL: URL) throws { |
| | | if let bodyPartError = bodyPartError { |
| | | throw bodyPartError |
| | | } |
| | | |
| | | if fileManager.fileExists(atPath: fileURL.path) { |
| | | throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL)) |
| | | } else if !fileURL.isFileURL { |
| | | throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL)) |
| | | } |
| | | |
| | | guard let outputStream = OutputStream(url: fileURL, append: false) else { |
| | | throw AFError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL)) |
| | | } |
| | | |
| | | outputStream.open() |
| | | defer { outputStream.close() } |
| | | |
| | | bodyParts.first?.hasInitialBoundary = true |
| | | bodyParts.last?.hasFinalBoundary = true |
| | | |
| | | for bodyPart in bodyParts { |
| | | try write(bodyPart, to: outputStream) |
| | | } |
| | | } |
| | | |
| | | // MARK: - Private - Body Part Encoding |
| | | |
| | | private func encode(_ bodyPart: BodyPart) throws -> Data { |
| | | var encoded = Data() |
| | | |
| | | let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData() |
| | | encoded.append(initialData) |
| | | |
| | | let headerData = encodeHeaders(for: bodyPart) |
| | | encoded.append(headerData) |
| | | |
| | | let bodyStreamData = try encodeBodyStream(for: bodyPart) |
| | | encoded.append(bodyStreamData) |
| | | |
| | | if bodyPart.hasFinalBoundary { |
| | | encoded.append(finalBoundaryData()) |
| | | } |
| | | |
| | | return encoded |
| | | } |
| | | |
| | | private func encodeHeaders(for bodyPart: BodyPart) -> Data { |
| | | let headerText = bodyPart.headers.map { "\($0.name): \($0.value)\(EncodingCharacters.crlf)" } |
| | | .joined() |
| | | + EncodingCharacters.crlf |
| | | |
| | | return Data(headerText.utf8) |
| | | } |
| | | |
| | | private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data { |
| | | let inputStream = bodyPart.bodyStream |
| | | inputStream.open() |
| | | defer { inputStream.close() } |
| | | |
| | | var encoded = Data() |
| | | |
| | | while inputStream.hasBytesAvailable { |
| | | var buffer = [UInt8](repeating: 0, count: streamBufferSize) |
| | | let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize) |
| | | |
| | | if let error = inputStream.streamError { |
| | | throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error)) |
| | | } |
| | | |
| | | if bytesRead > 0 { |
| | | encoded.append(buffer, count: bytesRead) |
| | | } else { |
| | | break |
| | | } |
| | | } |
| | | |
| | | guard UInt64(encoded.count) == bodyPart.bodyContentLength else { |
| | | let error = AFError.UnexpectedInputStreamLength(bytesExpected: bodyPart.bodyContentLength, |
| | | bytesRead: UInt64(encoded.count)) |
| | | throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error)) |
| | | } |
| | | |
| | | return encoded |
| | | } |
| | | |
| | | // MARK: - Private - Writing Body Part to Output Stream |
| | | |
| | | private func write(_ bodyPart: BodyPart, to outputStream: OutputStream) throws { |
| | | try writeInitialBoundaryData(for: bodyPart, to: outputStream) |
| | | try writeHeaderData(for: bodyPart, to: outputStream) |
| | | try writeBodyStream(for: bodyPart, to: outputStream) |
| | | try writeFinalBoundaryData(for: bodyPart, to: outputStream) |
| | | } |
| | | |
| | | private func writeInitialBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws { |
| | | let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData() |
| | | return try write(initialData, to: outputStream) |
| | | } |
| | | |
| | | private func writeHeaderData(for bodyPart: BodyPart, to outputStream: OutputStream) throws { |
| | | let headerData = encodeHeaders(for: bodyPart) |
| | | return try write(headerData, to: outputStream) |
| | | } |
| | | |
| | | private func writeBodyStream(for bodyPart: BodyPart, to outputStream: OutputStream) throws { |
| | | let inputStream = bodyPart.bodyStream |
| | | |
| | | inputStream.open() |
| | | defer { inputStream.close() } |
| | | |
| | | var bytesLeftToRead = bodyPart.bodyContentLength |
| | | while inputStream.hasBytesAvailable && bytesLeftToRead > 0 { |
| | | let bufferSize = min(streamBufferSize, Int(bytesLeftToRead)) |
| | | var buffer = [UInt8](repeating: 0, count: bufferSize) |
| | | let bytesRead = inputStream.read(&buffer, maxLength: bufferSize) |
| | | |
| | | if let streamError = inputStream.streamError { |
| | | throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: streamError)) |
| | | } |
| | | |
| | | if bytesRead > 0 { |
| | | if buffer.count != bytesRead { |
| | | buffer = Array(buffer[0..<bytesRead]) |
| | | } |
| | | |
| | | try write(&buffer, to: outputStream) |
| | | bytesLeftToRead -= UInt64(bytesRead) |
| | | } else { |
| | | break |
| | | } |
| | | } |
| | | } |
| | | |
| | | private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws { |
| | | if bodyPart.hasFinalBoundary { |
| | | return try write(finalBoundaryData(), to: outputStream) |
| | | } |
| | | } |
| | | |
| | | // MARK: - Private - Writing Buffered Data to Output Stream |
| | | |
| | | private func write(_ data: Data, to outputStream: OutputStream) throws { |
| | | var buffer = [UInt8](repeating: 0, count: data.count) |
| | | data.copyBytes(to: &buffer, count: data.count) |
| | | |
| | | return try write(&buffer, to: outputStream) |
| | | } |
| | | |
| | | private func write(_ buffer: inout [UInt8], to outputStream: OutputStream) throws { |
| | | var bytesToWrite = buffer.count |
| | | |
| | | while bytesToWrite > 0, outputStream.hasSpaceAvailable { |
| | | let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite) |
| | | |
| | | if let error = outputStream.streamError { |
| | | throw AFError.multipartEncodingFailed(reason: .outputStreamWriteFailed(error: error)) |
| | | } |
| | | |
| | | bytesToWrite -= bytesWritten |
| | | |
| | | if bytesToWrite > 0 { |
| | | buffer = Array(buffer[bytesWritten..<buffer.count]) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // MARK: - Private - Content Headers |
| | | |
| | | private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> HTTPHeaders { |
| | | var disposition = "form-data; name=\"\(name)\"" |
| | | if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" } |
| | | |
| | | var headers: HTTPHeaders = [.contentDisposition(disposition)] |
| | | if let mimeType = mimeType { headers.add(.contentType(mimeType)) } |
| | | |
| | | return headers |
| | | } |
| | | |
| | | // MARK: - Private - Boundary Encoding |
| | | |
| | | private func initialBoundaryData() -> Data { |
| | | BoundaryGenerator.boundaryData(forBoundaryType: .initial, boundary: boundary) |
| | | } |
| | | |
| | | private func encapsulatedBoundaryData() -> Data { |
| | | BoundaryGenerator.boundaryData(forBoundaryType: .encapsulated, boundary: boundary) |
| | | } |
| | | |
| | | private func finalBoundaryData() -> Data { |
| | | BoundaryGenerator.boundaryData(forBoundaryType: .final, boundary: boundary) |
| | | } |
| | | |
| | | // MARK: - Private - Errors |
| | | |
| | | private func setBodyPartError(withReason reason: AFError.MultipartEncodingFailureReason) { |
| | | guard bodyPartError == nil else { return } |
| | | bodyPartError = AFError.multipartEncodingFailed(reason: reason) |
| | | } |
| | | } |
| | | |
| | | #if canImport(UniformTypeIdentifiers) |
| | | import UniformTypeIdentifiers |
| | | |
| | | extension MultipartFormData { |
| | | // MARK: - Private - Mime Type |
| | | |
| | | private func mimeType(forPathExtension pathExtension: String) -> String { |
| | | #if swift(>=5.9) |
| | | if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, visionOS 1, *) { |
| | | return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream" |
| | | } else { |
| | | if |
| | | let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(), |
| | | let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() { |
| | | return contentType as String |
| | | } |
| | | |
| | | return "application/octet-stream" |
| | | } |
| | | #else |
| | | if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) { |
| | | return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream" |
| | | } else { |
| | | if |
| | | let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(), |
| | | let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() { |
| | | return contentType as String |
| | | } |
| | | |
| | | return "application/octet-stream" |
| | | } |
| | | #endif |
| | | } |
| | | } |
| | | |
| | | #else |
| | | |
| | | extension MultipartFormData { |
| | | // MARK: - Private - Mime Type |
| | | |
| | | private func mimeType(forPathExtension pathExtension: String) -> String { |
| | | #if canImport(CoreServices) || canImport(MobileCoreServices) |
| | | if |
| | | let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(), |
| | | let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() { |
| | | return contentType as String |
| | | } |
| | | #endif |
| | | |
| | | return "application/octet-stream" |
| | | } |
| | | } |
| | | |
| | | #endif |
New file |
| | |
| | | // |
| | | // MultipartUpload.swift |
| | | // |
| | | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | /// Internal type which encapsulates a `MultipartFormData` upload. |
| | | final class MultipartUpload { |
| | | lazy var result = Result { try build() } |
| | | |
| | | private let multipartFormData: Protected<MultipartFormData> |
| | | |
| | | let encodingMemoryThreshold: UInt64 |
| | | let request: URLRequestConvertible |
| | | let fileManager: FileManager |
| | | |
| | | init(encodingMemoryThreshold: UInt64, |
| | | request: URLRequestConvertible, |
| | | multipartFormData: MultipartFormData) { |
| | | self.encodingMemoryThreshold = encodingMemoryThreshold |
| | | self.request = request |
| | | fileManager = multipartFormData.fileManager |
| | | self.multipartFormData = Protected(multipartFormData) |
| | | } |
| | | |
| | | func build() throws -> UploadRequest.Uploadable { |
| | | let uploadable: UploadRequest.Uploadable |
| | | if multipartFormData.contentLength < encodingMemoryThreshold { |
| | | let data = try multipartFormData.read { try $0.encode() } |
| | | |
| | | uploadable = .data(data) |
| | | } else { |
| | | let tempDirectoryURL = fileManager.temporaryDirectory |
| | | let directoryURL = tempDirectoryURL.appendingPathComponent("org.alamofire.manager/multipart.form.data") |
| | | let fileName = UUID().uuidString |
| | | let fileURL = directoryURL.appendingPathComponent(fileName) |
| | | |
| | | try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) |
| | | |
| | | do { |
| | | try multipartFormData.read { try $0.writeEncodedData(to: fileURL) } |
| | | } catch { |
| | | // Cleanup after attempted write if it fails. |
| | | try? fileManager.removeItem(at: fileURL) |
| | | throw error |
| | | } |
| | | |
| | | uploadable = .file(fileURL, shouldRemove: true) |
| | | } |
| | | |
| | | return uploadable |
| | | } |
| | | } |
| | | |
| | | extension MultipartUpload: UploadConvertible { |
| | | func asURLRequest() throws -> URLRequest { |
| | | var urlRequest = try request.asURLRequest() |
| | | |
| | | multipartFormData.read { multipartFormData in |
| | | urlRequest.headers.add(.contentType(multipartFormData.contentType)) |
| | | } |
| | | |
| | | return urlRequest |
| | | } |
| | | |
| | | func createUploadable() throws -> UploadRequest.Uploadable { |
| | | try result.get() |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // NetworkReachabilityManager.swift |
| | | // |
| | | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | #if canImport(SystemConfiguration) |
| | | |
| | | import Foundation |
| | | import SystemConfiguration |
| | | |
| | | /// The `NetworkReachabilityManager` class listens for reachability changes of hosts and addresses for both cellular and |
| | | /// WiFi network interfaces. |
| | | /// |
| | | /// Reachability can be used to determine background information about why a network operation failed, or to retry |
| | | /// network requests when a connection is established. It should not be used to prevent a user from initiating a network |
| | | /// request, as it's possible that an initial request may be required to establish reachability. |
| | | open class NetworkReachabilityManager { |
| | | /// Defines the various states of network reachability. |
| | | public enum NetworkReachabilityStatus { |
| | | /// It is unknown whether the network is reachable. |
| | | case unknown |
| | | /// The network is not reachable. |
| | | case notReachable |
| | | /// The network is reachable on the associated `ConnectionType`. |
| | | case reachable(ConnectionType) |
| | | |
| | | init(_ flags: SCNetworkReachabilityFlags) { |
| | | guard flags.isActuallyReachable else { self = .notReachable; return } |
| | | |
| | | var networkStatus: NetworkReachabilityStatus = .reachable(.ethernetOrWiFi) |
| | | |
| | | if flags.isCellular { networkStatus = .reachable(.cellular) } |
| | | |
| | | self = networkStatus |
| | | } |
| | | |
| | | /// Defines the various connection types detected by reachability flags. |
| | | public enum ConnectionType { |
| | | /// The connection type is either over Ethernet or WiFi. |
| | | case ethernetOrWiFi |
| | | /// The connection type is a cellular connection. |
| | | case cellular |
| | | } |
| | | } |
| | | |
| | | /// A closure executed when the network reachability status changes. The closure takes a single argument: the |
| | | /// network reachability status. |
| | | public typealias Listener = (NetworkReachabilityStatus) -> Void |
| | | |
| | | /// Default `NetworkReachabilityManager` for the zero address and a `listenerQueue` of `.main`. |
| | | public static let `default` = NetworkReachabilityManager() |
| | | |
| | | // MARK: - Properties |
| | | |
| | | /// Whether the network is currently reachable. |
| | | open var isReachable: Bool { isReachableOnCellular || isReachableOnEthernetOrWiFi } |
| | | |
| | | /// Whether the network is currently reachable over the cellular interface. |
| | | /// |
| | | /// - Note: Using this property to decide whether to make a high or low bandwidth request is not recommended. |
| | | /// Instead, set the `allowsCellularAccess` on any `URLRequest`s being issued. |
| | | /// |
| | | open var isReachableOnCellular: Bool { status == .reachable(.cellular) } |
| | | |
| | | /// Whether the network is currently reachable over Ethernet or WiFi interface. |
| | | open var isReachableOnEthernetOrWiFi: Bool { status == .reachable(.ethernetOrWiFi) } |
| | | |
| | | /// `DispatchQueue` on which reachability will update. |
| | | public let reachabilityQueue = DispatchQueue(label: "org.alamofire.reachabilityQueue") |
| | | |
| | | /// Flags of the current reachability type, if any. |
| | | open var flags: SCNetworkReachabilityFlags? { |
| | | var flags = SCNetworkReachabilityFlags() |
| | | |
| | | return SCNetworkReachabilityGetFlags(reachability, &flags) ? flags : nil |
| | | } |
| | | |
| | | /// The current network reachability status. |
| | | open var status: NetworkReachabilityStatus { |
| | | flags.map(NetworkReachabilityStatus.init) ?? .unknown |
| | | } |
| | | |
| | | /// Mutable state storage. |
| | | struct MutableState { |
| | | /// A closure executed when the network reachability status changes. |
| | | var listener: Listener? |
| | | /// `DispatchQueue` on which listeners will be called. |
| | | var listenerQueue: DispatchQueue? |
| | | /// Previously calculated status. |
| | | var previousStatus: NetworkReachabilityStatus? |
| | | } |
| | | |
| | | /// `SCNetworkReachability` instance providing notifications. |
| | | private let reachability: SCNetworkReachability |
| | | |
| | | /// Protected storage for mutable state. |
| | | private let mutableState = Protected(MutableState()) |
| | | |
| | | // MARK: - Initialization |
| | | |
| | | /// Creates an instance with the specified host. |
| | | /// |
| | | /// - Note: The `host` value must *not* contain a scheme, just the hostname. |
| | | /// |
| | | /// - Parameters: |
| | | /// - host: Host used to evaluate network reachability. Must *not* include the scheme (e.g. `https`). |
| | | public convenience init?(host: String) { |
| | | guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil } |
| | | |
| | | self.init(reachability: reachability) |
| | | } |
| | | |
| | | /// Creates an instance that monitors the address 0.0.0.0. |
| | | /// |
| | | /// Reachability treats the 0.0.0.0 address as a special token that causes it to monitor the general routing |
| | | /// status of the device, both IPv4 and IPv6. |
| | | public convenience init?() { |
| | | var zero = sockaddr() |
| | | zero.sa_len = UInt8(MemoryLayout<sockaddr>.size) |
| | | zero.sa_family = sa_family_t(AF_INET) |
| | | |
| | | guard let reachability = SCNetworkReachabilityCreateWithAddress(nil, &zero) else { return nil } |
| | | |
| | | self.init(reachability: reachability) |
| | | } |
| | | |
| | | private init(reachability: SCNetworkReachability) { |
| | | self.reachability = reachability |
| | | } |
| | | |
| | | deinit { |
| | | stopListening() |
| | | } |
| | | |
| | | // MARK: - Listening |
| | | |
| | | /// Starts listening for changes in network reachability status. |
| | | /// |
| | | /// - Note: Stops and removes any existing listener. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which to call the `listener` closure. `.main` by default. |
| | | /// - listener: `Listener` closure called when reachability changes. |
| | | /// |
| | | /// - Returns: `true` if listening was started successfully, `false` otherwise. |
| | | @discardableResult |
| | | open func startListening(onQueue queue: DispatchQueue = .main, |
| | | onUpdatePerforming listener: @escaping Listener) -> Bool { |
| | | stopListening() |
| | | |
| | | mutableState.write { state in |
| | | state.listenerQueue = queue |
| | | state.listener = listener |
| | | } |
| | | |
| | | let weakManager = WeakManager(manager: self) |
| | | |
| | | var context = SCNetworkReachabilityContext( |
| | | version: 0, |
| | | info: Unmanaged.passUnretained(weakManager).toOpaque(), |
| | | retain: { info in |
| | | let unmanaged = Unmanaged<WeakManager>.fromOpaque(info) |
| | | _ = unmanaged.retain() |
| | | |
| | | return UnsafeRawPointer(unmanaged.toOpaque()) |
| | | }, |
| | | release: { info in |
| | | let unmanaged = Unmanaged<WeakManager>.fromOpaque(info) |
| | | unmanaged.release() |
| | | }, |
| | | copyDescription: { info in |
| | | let unmanaged = Unmanaged<WeakManager>.fromOpaque(info) |
| | | let weakManager = unmanaged.takeUnretainedValue() |
| | | let description = weakManager.manager?.flags?.readableDescription ?? "nil" |
| | | |
| | | return Unmanaged.passRetained(description as CFString) |
| | | } |
| | | ) |
| | | let callback: SCNetworkReachabilityCallBack = { _, flags, info in |
| | | guard let info = info else { return } |
| | | |
| | | let weakManager = Unmanaged<WeakManager>.fromOpaque(info).takeUnretainedValue() |
| | | weakManager.manager?.notifyListener(flags) |
| | | } |
| | | |
| | | let queueAdded = SCNetworkReachabilitySetDispatchQueue(reachability, reachabilityQueue) |
| | | let callbackAdded = SCNetworkReachabilitySetCallback(reachability, callback, &context) |
| | | |
| | | // Manually call listener to give initial state, since the framework may not. |
| | | if let currentFlags = flags { |
| | | reachabilityQueue.async { |
| | | self.notifyListener(currentFlags) |
| | | } |
| | | } |
| | | |
| | | return callbackAdded && queueAdded |
| | | } |
| | | |
| | | /// Stops listening for changes in network reachability status. |
| | | open func stopListening() { |
| | | SCNetworkReachabilitySetCallback(reachability, nil, nil) |
| | | SCNetworkReachabilitySetDispatchQueue(reachability, nil) |
| | | mutableState.write { state in |
| | | state.listener = nil |
| | | state.listenerQueue = nil |
| | | state.previousStatus = nil |
| | | } |
| | | } |
| | | |
| | | // MARK: - Internal - Listener Notification |
| | | |
| | | /// Calls the `listener` closure of the `listenerQueue` if the computed status hasn't changed. |
| | | /// |
| | | /// - Note: Should only be called from the `reachabilityQueue`. |
| | | /// |
| | | /// - Parameter flags: `SCNetworkReachabilityFlags` to use to calculate the status. |
| | | func notifyListener(_ flags: SCNetworkReachabilityFlags) { |
| | | let newStatus = NetworkReachabilityStatus(flags) |
| | | |
| | | mutableState.write { state in |
| | | guard state.previousStatus != newStatus else { return } |
| | | |
| | | state.previousStatus = newStatus |
| | | |
| | | let listener = state.listener |
| | | state.listenerQueue?.async { listener?(newStatus) } |
| | | } |
| | | } |
| | | |
| | | private final class WeakManager { |
| | | weak var manager: NetworkReachabilityManager? |
| | | |
| | | init(manager: NetworkReachabilityManager?) { |
| | | self.manager = manager |
| | | } |
| | | } |
| | | } |
| | | |
| | | // MARK: - |
| | | |
| | | extension NetworkReachabilityManager.NetworkReachabilityStatus: Equatable {} |
| | | |
| | | extension SCNetworkReachabilityFlags { |
| | | var isReachable: Bool { contains(.reachable) } |
| | | var isConnectionRequired: Bool { contains(.connectionRequired) } |
| | | var canConnectAutomatically: Bool { contains(.connectionOnDemand) || contains(.connectionOnTraffic) } |
| | | var canConnectWithoutUserInteraction: Bool { canConnectAutomatically && !contains(.interventionRequired) } |
| | | var isActuallyReachable: Bool { isReachable && (!isConnectionRequired || canConnectWithoutUserInteraction) } |
| | | var isCellular: Bool { |
| | | #if os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS)) |
| | | return contains(.isWWAN) |
| | | #else |
| | | return false |
| | | #endif |
| | | } |
| | | |
| | | /// Human readable `String` for all states, to help with debugging. |
| | | var readableDescription: String { |
| | | let W = isCellular ? "W" : "-" |
| | | let R = isReachable ? "R" : "-" |
| | | let c = isConnectionRequired ? "c" : "-" |
| | | let t = contains(.transientConnection) ? "t" : "-" |
| | | let i = contains(.interventionRequired) ? "i" : "-" |
| | | let C = contains(.connectionOnTraffic) ? "C" : "-" |
| | | let D = contains(.connectionOnDemand) ? "D" : "-" |
| | | let l = contains(.isLocalAddress) ? "l" : "-" |
| | | let d = contains(.isDirect) ? "d" : "-" |
| | | let a = contains(.connectionAutomatic) ? "a" : "-" |
| | | |
| | | return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)\(a)" |
| | | } |
| | | } |
| | | #endif |
New file |
| | |
| | | // |
| | | // Notifications.swift |
| | | // |
| | | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | extension Request { |
| | | /// Posted when a `Request` is resumed. The `Notification` contains the resumed `Request`. |
| | | public static let didResumeNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didResume") |
| | | /// Posted when a `Request` is suspended. The `Notification` contains the suspended `Request`. |
| | | public static let didSuspendNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didSuspend") |
| | | /// Posted when a `Request` is cancelled. The `Notification` contains the cancelled `Request`. |
| | | public static let didCancelNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCancel") |
| | | /// Posted when a `Request` is finished. The `Notification` contains the completed `Request`. |
| | | public static let didFinishNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didFinish") |
| | | |
| | | /// Posted when a `URLSessionTask` is resumed. The `Notification` contains the `Request` associated with the `URLSessionTask`. |
| | | public static let didResumeTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didResumeTask") |
| | | /// Posted when a `URLSessionTask` is suspended. The `Notification` contains the `Request` associated with the `URLSessionTask`. |
| | | public static let didSuspendTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didSuspendTask") |
| | | /// Posted when a `URLSessionTask` is cancelled. The `Notification` contains the `Request` associated with the `URLSessionTask`. |
| | | public static let didCancelTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCancelTask") |
| | | /// Posted when a `URLSessionTask` is completed. The `Notification` contains the `Request` associated with the `URLSessionTask`. |
| | | public static let didCompleteTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCompleteTask") |
| | | } |
| | | |
| | | // MARK: - |
| | | |
| | | extension Notification { |
| | | /// The `Request` contained by the instance's `userInfo`, `nil` otherwise. |
| | | public var request: Request? { |
| | | userInfo?[String.requestKey] as? Request |
| | | } |
| | | |
| | | /// Convenience initializer for a `Notification` containing a `Request` payload. |
| | | /// |
| | | /// - Parameters: |
| | | /// - name: The name of the notification. |
| | | /// - request: The `Request` payload. |
| | | init(name: Notification.Name, request: Request) { |
| | | self.init(name: name, object: nil, userInfo: [String.requestKey: request]) |
| | | } |
| | | } |
| | | |
| | | extension NotificationCenter { |
| | | /// Convenience function for posting notifications with `Request` payloads. |
| | | /// |
| | | /// - Parameters: |
| | | /// - name: The name of the notification. |
| | | /// - request: The `Request` payload. |
| | | func postNotification(named name: Notification.Name, with request: Request) { |
| | | let notification = Notification(name: name, request: request) |
| | | post(notification) |
| | | } |
| | | } |
| | | |
| | | extension String { |
| | | /// User info dictionary key representing the `Request` associated with the notification. |
| | | fileprivate static let requestKey = "org.alamofire.notification.key.request" |
| | | } |
| | | |
| | | /// `EventMonitor` that provides Alamofire's notifications. |
| | | public final class AlamofireNotifications: EventMonitor { |
| | | public func requestDidResume(_ request: Request) { |
| | | NotificationCenter.default.postNotification(named: Request.didResumeNotification, with: request) |
| | | } |
| | | |
| | | public func requestDidSuspend(_ request: Request) { |
| | | NotificationCenter.default.postNotification(named: Request.didSuspendNotification, with: request) |
| | | } |
| | | |
| | | public func requestDidCancel(_ request: Request) { |
| | | NotificationCenter.default.postNotification(named: Request.didCancelNotification, with: request) |
| | | } |
| | | |
| | | public func requestDidFinish(_ request: Request) { |
| | | NotificationCenter.default.postNotification(named: Request.didFinishNotification, with: request) |
| | | } |
| | | |
| | | public func request(_ request: Request, didResumeTask task: URLSessionTask) { |
| | | NotificationCenter.default.postNotification(named: Request.didResumeTaskNotification, with: request) |
| | | } |
| | | |
| | | public func request(_ request: Request, didSuspendTask task: URLSessionTask) { |
| | | NotificationCenter.default.postNotification(named: Request.didSuspendTaskNotification, with: request) |
| | | } |
| | | |
| | | public func request(_ request: Request, didCancelTask task: URLSessionTask) { |
| | | NotificationCenter.default.postNotification(named: Request.didCancelTaskNotification, with: request) |
| | | } |
| | | |
| | | public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?) { |
| | | NotificationCenter.default.postNotification(named: Request.didCompleteTaskNotification, with: request) |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // OperationQueue+Alamofire.swift |
| | | // |
| | | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | extension OperationQueue { |
| | | /// Creates an instance using the provided parameters. |
| | | /// |
| | | /// - Parameters: |
| | | /// - qualityOfService: `QualityOfService` to be applied to the queue. `.default` by default. |
| | | /// - maxConcurrentOperationCount: Maximum concurrent operations. |
| | | /// `OperationQueue.defaultMaxConcurrentOperationCount` by default. |
| | | /// - underlyingQueue: Underlying `DispatchQueue`. `nil` by default. |
| | | /// - name: Name for the queue. `nil` by default. |
| | | /// - startSuspended: Whether the queue starts suspended. `false` by default. |
| | | convenience init(qualityOfService: QualityOfService = .default, |
| | | maxConcurrentOperationCount: Int = OperationQueue.defaultMaxConcurrentOperationCount, |
| | | underlyingQueue: DispatchQueue? = nil, |
| | | name: String? = nil, |
| | | startSuspended: Bool = false) { |
| | | self.init() |
| | | self.qualityOfService = qualityOfService |
| | | self.maxConcurrentOperationCount = maxConcurrentOperationCount |
| | | self.underlyingQueue = underlyingQueue |
| | | self.name = name |
| | | isSuspended = startSuspended |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // ParameterEncoder.swift |
| | | // |
| | | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | /// A type that can encode any `Encodable` type into a `URLRequest`. |
| | | public protocol ParameterEncoder { |
| | | /// Encode the provided `Encodable` parameters into `request`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - parameters: The `Encodable` parameter value. |
| | | /// - request: The `URLRequest` into which to encode the parameters. |
| | | /// |
| | | /// - Returns: A `URLRequest` with the result of the encoding. |
| | | /// - Throws: An `Error` when encoding fails. For Alamofire provided encoders, this will be an instance of |
| | | /// `AFError.parameterEncoderFailed` with an associated `ParameterEncoderFailureReason`. |
| | | func encode<Parameters: Encodable>(_ parameters: Parameters?, into request: URLRequest) throws -> URLRequest |
| | | } |
| | | |
| | | /// A `ParameterEncoder` that encodes types as JSON body data. |
| | | /// |
| | | /// If no `Content-Type` header is already set on the provided `URLRequest`s, it's set to `application/json`. |
| | | open class JSONParameterEncoder: ParameterEncoder { |
| | | /// Returns an encoder with default parameters. |
| | | public static var `default`: JSONParameterEncoder { JSONParameterEncoder() } |
| | | |
| | | /// Returns an encoder with `JSONEncoder.outputFormatting` set to `.prettyPrinted`. |
| | | public static var prettyPrinted: JSONParameterEncoder { |
| | | let encoder = JSONEncoder() |
| | | encoder.outputFormatting = .prettyPrinted |
| | | |
| | | return JSONParameterEncoder(encoder: encoder) |
| | | } |
| | | |
| | | /// Returns an encoder with `JSONEncoder.outputFormatting` set to `.sortedKeys`. |
| | | @available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *) |
| | | public static var sortedKeys: JSONParameterEncoder { |
| | | let encoder = JSONEncoder() |
| | | encoder.outputFormatting = .sortedKeys |
| | | |
| | | return JSONParameterEncoder(encoder: encoder) |
| | | } |
| | | |
| | | /// `JSONEncoder` used to encode parameters. |
| | | public let encoder: JSONEncoder |
| | | |
| | | /// Creates an instance with the provided `JSONEncoder`. |
| | | /// |
| | | /// - Parameter encoder: The `JSONEncoder`. `JSONEncoder()` by default. |
| | | public init(encoder: JSONEncoder = JSONEncoder()) { |
| | | self.encoder = encoder |
| | | } |
| | | |
| | | open func encode<Parameters: Encodable>(_ parameters: Parameters?, |
| | | into request: URLRequest) throws -> URLRequest { |
| | | guard let parameters = parameters else { return request } |
| | | |
| | | var request = request |
| | | |
| | | do { |
| | | let data = try encoder.encode(parameters) |
| | | request.httpBody = data |
| | | if request.headers["Content-Type"] == nil { |
| | | request.headers.update(.contentType("application/json")) |
| | | } |
| | | } catch { |
| | | throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) |
| | | } |
| | | |
| | | return request |
| | | } |
| | | } |
| | | |
| | | extension ParameterEncoder where Self == JSONParameterEncoder { |
| | | /// Provides a default `JSONParameterEncoder` instance. |
| | | public static var json: JSONParameterEncoder { JSONParameterEncoder() } |
| | | |
| | | /// Creates a `JSONParameterEncoder` using the provided `JSONEncoder`. |
| | | /// |
| | | /// - Parameter encoder: `JSONEncoder` used to encode parameters. `JSONEncoder()` by default. |
| | | /// - Returns: The `JSONParameterEncoder`. |
| | | public static func json(encoder: JSONEncoder = JSONEncoder()) -> JSONParameterEncoder { |
| | | JSONParameterEncoder(encoder: encoder) |
| | | } |
| | | } |
| | | |
| | | /// A `ParameterEncoder` that encodes types as URL-encoded query strings to be set on the URL or as body data, depending |
| | | /// on the `Destination` set. |
| | | /// |
| | | /// If no `Content-Type` header is already set on the provided `URLRequest`s, it will be set to |
| | | /// `application/x-www-form-urlencoded; charset=utf-8`. |
| | | /// |
| | | /// Encoding behavior can be customized by passing an instance of `URLEncodedFormEncoder` to the initializer. |
| | | open class URLEncodedFormParameterEncoder: ParameterEncoder { |
| | | /// Defines where the URL-encoded string should be set for each `URLRequest`. |
| | | public enum Destination { |
| | | /// Applies the encoded query string to any existing query string for `.get`, `.head`, and `.delete` request. |
| | | /// Sets it to the `httpBody` for all other methods. |
| | | case methodDependent |
| | | /// Applies the encoded query string to any existing query string from the `URLRequest`. |
| | | case queryString |
| | | /// Applies the encoded query string to the `httpBody` of the `URLRequest`. |
| | | case httpBody |
| | | |
| | | /// Determines whether the URL-encoded string should be applied to the `URLRequest`'s `url`. |
| | | /// |
| | | /// - Parameter method: The `HTTPMethod`. |
| | | /// |
| | | /// - Returns: Whether the URL-encoded string should be applied to a `URL`. |
| | | func encodesParametersInURL(for method: HTTPMethod) -> Bool { |
| | | switch self { |
| | | case .methodDependent: return [.get, .head, .delete].contains(method) |
| | | case .queryString: return true |
| | | case .httpBody: return false |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// Returns an encoder with default parameters. |
| | | public static var `default`: URLEncodedFormParameterEncoder { URLEncodedFormParameterEncoder() } |
| | | |
| | | /// The `URLEncodedFormEncoder` to use. |
| | | public let encoder: URLEncodedFormEncoder |
| | | |
| | | /// The `Destination` for the URL-encoded string. |
| | | public let destination: Destination |
| | | |
| | | /// Creates an instance with the provided `URLEncodedFormEncoder` instance and `Destination` value. |
| | | /// |
| | | /// - Parameters: |
| | | /// - encoder: The `URLEncodedFormEncoder`. `URLEncodedFormEncoder()` by default. |
| | | /// - destination: The `Destination`. `.methodDependent` by default. |
| | | public init(encoder: URLEncodedFormEncoder = URLEncodedFormEncoder(), destination: Destination = .methodDependent) { |
| | | self.encoder = encoder |
| | | self.destination = destination |
| | | } |
| | | |
| | | open func encode<Parameters: Encodable>(_ parameters: Parameters?, |
| | | into request: URLRequest) throws -> URLRequest { |
| | | guard let parameters = parameters else { return request } |
| | | |
| | | var request = request |
| | | |
| | | guard let url = request.url else { |
| | | throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url)) |
| | | } |
| | | |
| | | guard let method = request.method else { |
| | | let rawValue = request.method?.rawValue ?? "nil" |
| | | throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.httpMethod(rawValue: rawValue))) |
| | | } |
| | | |
| | | if destination.encodesParametersInURL(for: method), |
| | | var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { |
| | | let query: String = try Result<String, Error> { try encoder.encode(parameters) } |
| | | .mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.get() |
| | | let newQueryString = [components.percentEncodedQuery, query].compactMap { $0 }.joinedWithAmpersands() |
| | | components.percentEncodedQuery = newQueryString.isEmpty ? nil : newQueryString |
| | | |
| | | guard let newURL = components.url else { |
| | | throw AFError.parameterEncoderFailed(reason: .missingRequiredComponent(.url)) |
| | | } |
| | | |
| | | request.url = newURL |
| | | } else { |
| | | if request.headers["Content-Type"] == nil { |
| | | request.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8")) |
| | | } |
| | | |
| | | request.httpBody = try Result<Data, Error> { try encoder.encode(parameters) } |
| | | .mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.get() |
| | | } |
| | | |
| | | return request |
| | | } |
| | | } |
| | | |
| | | extension ParameterEncoder where Self == URLEncodedFormParameterEncoder { |
| | | /// Provides a default `URLEncodedFormParameterEncoder` instance. |
| | | public static var urlEncodedForm: URLEncodedFormParameterEncoder { URLEncodedFormParameterEncoder() } |
| | | |
| | | /// Creates a `URLEncodedFormParameterEncoder` with the provided encoder and destination. |
| | | /// |
| | | /// - Parameters: |
| | | /// - encoder: `URLEncodedFormEncoder` used to encode the parameters. `URLEncodedFormEncoder()` by default. |
| | | /// - destination: `Destination` to which to encode the parameters. `.methodDependent` by default. |
| | | /// - Returns: The `URLEncodedFormParameterEncoder`. |
| | | public static func urlEncodedForm(encoder: URLEncodedFormEncoder = URLEncodedFormEncoder(), |
| | | destination: URLEncodedFormParameterEncoder.Destination = .methodDependent) -> URLEncodedFormParameterEncoder { |
| | | URLEncodedFormParameterEncoder(encoder: encoder, destination: destination) |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // ParameterEncoding.swift |
| | | // |
| | | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | /// A dictionary of parameters to apply to a `URLRequest`. |
| | | public typealias Parameters = [String: Any] |
| | | |
| | | /// A type used to define how a set of parameters are applied to a `URLRequest`. |
| | | public protocol ParameterEncoding { |
| | | /// Creates a `URLRequest` by encoding parameters and applying them on the passed request. |
| | | /// |
| | | /// - Parameters: |
| | | /// - urlRequest: `URLRequestConvertible` value onto which parameters will be encoded. |
| | | /// - parameters: `Parameters` to encode onto the request. |
| | | /// |
| | | /// - Returns: The encoded `URLRequest`. |
| | | /// - Throws: Any `Error` produced during parameter encoding. |
| | | func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest |
| | | } |
| | | |
| | | // MARK: - |
| | | |
| | | /// Creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP |
| | | /// body of the URL request. Whether the query string is set or appended to any existing URL query string or set as |
| | | /// the HTTP body depends on the destination of the encoding. |
| | | /// |
| | | /// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to |
| | | /// `application/x-www-form-urlencoded; charset=utf-8`. |
| | | /// |
| | | /// There is no published specification for how to encode collection types. By default the convention of appending |
| | | /// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for |
| | | /// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the |
| | | /// square brackets appended to array keys. |
| | | /// |
| | | /// `BoolEncoding` can be used to configure how boolean values are encoded. The default behavior is to encode |
| | | /// `true` as 1 and `false` as 0. |
| | | public struct URLEncoding: ParameterEncoding { |
| | | // MARK: Helper Types |
| | | |
| | | /// Defines whether the url-encoded query string is applied to the existing query string or HTTP body of the |
| | | /// resulting URL request. |
| | | public enum Destination { |
| | | /// Applies encoded query string result to existing query string for `GET`, `HEAD` and `DELETE` requests and |
| | | /// sets as the HTTP body for requests with any other HTTP method. |
| | | case methodDependent |
| | | /// Sets or appends encoded query string result to existing query string. |
| | | case queryString |
| | | /// Sets encoded query string result as the HTTP body of the URL request. |
| | | case httpBody |
| | | |
| | | func encodesParametersInURL(for method: HTTPMethod) -> Bool { |
| | | switch self { |
| | | case .methodDependent: return [.get, .head, .delete].contains(method) |
| | | case .queryString: return true |
| | | case .httpBody: return false |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// Configures how `Array` parameters are encoded. |
| | | public enum ArrayEncoding { |
| | | /// An empty set of square brackets is appended to the key for every value. This is the default behavior. |
| | | case brackets |
| | | /// No brackets are appended. The key is encoded as is. |
| | | case noBrackets |
| | | /// Brackets containing the item index are appended. This matches the jQuery and Node.js behavior. |
| | | case indexInBrackets |
| | | /// Provide a custom array key encoding with the given closure. |
| | | case custom((_ key: String, _ index: Int) -> String) |
| | | |
| | | func encode(key: String, atIndex index: Int) -> String { |
| | | switch self { |
| | | case .brackets: |
| | | return "\(key)[]" |
| | | case .noBrackets: |
| | | return key |
| | | case .indexInBrackets: |
| | | return "\(key)[\(index)]" |
| | | case let .custom(encoding): |
| | | return encoding(key, index) |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// Configures how `Bool` parameters are encoded. |
| | | public enum BoolEncoding { |
| | | /// Encode `true` as `1` and `false` as `0`. This is the default behavior. |
| | | case numeric |
| | | /// Encode `true` and `false` as string literals. |
| | | case literal |
| | | |
| | | func encode(value: Bool) -> String { |
| | | switch self { |
| | | case .numeric: |
| | | return value ? "1" : "0" |
| | | case .literal: |
| | | return value ? "true" : "false" |
| | | } |
| | | } |
| | | } |
| | | |
| | | // MARK: Properties |
| | | |
| | | /// Returns a default `URLEncoding` instance with a `.methodDependent` destination. |
| | | public static var `default`: URLEncoding { URLEncoding() } |
| | | |
| | | /// Returns a `URLEncoding` instance with a `.queryString` destination. |
| | | public static var queryString: URLEncoding { URLEncoding(destination: .queryString) } |
| | | |
| | | /// Returns a `URLEncoding` instance with an `.httpBody` destination. |
| | | public static var httpBody: URLEncoding { URLEncoding(destination: .httpBody) } |
| | | |
| | | /// The destination defining where the encoded query string is to be applied to the URL request. |
| | | public let destination: Destination |
| | | |
| | | /// The encoding to use for `Array` parameters. |
| | | public let arrayEncoding: ArrayEncoding |
| | | |
| | | /// The encoding to use for `Bool` parameters. |
| | | public let boolEncoding: BoolEncoding |
| | | |
| | | // MARK: Initialization |
| | | |
| | | /// Creates an instance using the specified parameters. |
| | | /// |
| | | /// - Parameters: |
| | | /// - destination: `Destination` defining where the encoded query string will be applied. `.methodDependent` by |
| | | /// default. |
| | | /// - arrayEncoding: `ArrayEncoding` to use. `.brackets` by default. |
| | | /// - boolEncoding: `BoolEncoding` to use. `.numeric` by default. |
| | | public init(destination: Destination = .methodDependent, |
| | | arrayEncoding: ArrayEncoding = .brackets, |
| | | boolEncoding: BoolEncoding = .numeric) { |
| | | self.destination = destination |
| | | self.arrayEncoding = arrayEncoding |
| | | self.boolEncoding = boolEncoding |
| | | } |
| | | |
| | | // MARK: Encoding |
| | | |
| | | public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { |
| | | var urlRequest = try urlRequest.asURLRequest() |
| | | |
| | | guard let parameters = parameters else { return urlRequest } |
| | | |
| | | if let method = urlRequest.method, destination.encodesParametersInURL(for: method) { |
| | | guard let url = urlRequest.url else { |
| | | throw AFError.parameterEncodingFailed(reason: .missingURL) |
| | | } |
| | | |
| | | if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty { |
| | | let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters) |
| | | urlComponents.percentEncodedQuery = percentEncodedQuery |
| | | urlRequest.url = urlComponents.url |
| | | } |
| | | } else { |
| | | if urlRequest.headers["Content-Type"] == nil { |
| | | urlRequest.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8")) |
| | | } |
| | | |
| | | urlRequest.httpBody = Data(query(parameters).utf8) |
| | | } |
| | | |
| | | return urlRequest |
| | | } |
| | | |
| | | /// Creates a percent-escaped, URL encoded query string components from the given key-value pair recursively. |
| | | /// |
| | | /// - Parameters: |
| | | /// - key: Key of the query component. |
| | | /// - value: Value of the query component. |
| | | /// |
| | | /// - Returns: The percent-escaped, URL encoded query string components. |
| | | public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] { |
| | | var components: [(String, String)] = [] |
| | | switch value { |
| | | case let dictionary as [String: Any]: |
| | | for (nestedKey, value) in dictionary { |
| | | components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value) |
| | | } |
| | | case let array as [Any]: |
| | | for (index, value) in array.enumerated() { |
| | | components += queryComponents(fromKey: arrayEncoding.encode(key: key, atIndex: index), value: value) |
| | | } |
| | | case let number as NSNumber: |
| | | if number.isBool { |
| | | components.append((escape(key), escape(boolEncoding.encode(value: number.boolValue)))) |
| | | } else { |
| | | components.append((escape(key), escape("\(number)"))) |
| | | } |
| | | case let bool as Bool: |
| | | components.append((escape(key), escape(boolEncoding.encode(value: bool)))) |
| | | default: |
| | | components.append((escape(key), escape("\(value)"))) |
| | | } |
| | | return components |
| | | } |
| | | |
| | | /// Creates a percent-escaped string following RFC 3986 for a query string key or value. |
| | | /// |
| | | /// - Parameter string: `String` to be percent-escaped. |
| | | /// |
| | | /// - Returns: The percent-escaped `String`. |
| | | public func escape(_ string: String) -> String { |
| | | string.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? string |
| | | } |
| | | |
| | | private func query(_ parameters: [String: Any]) -> String { |
| | | var components: [(String, String)] = [] |
| | | |
| | | for key in parameters.keys.sorted(by: <) { |
| | | let value = parameters[key]! |
| | | components += queryComponents(fromKey: key, value: value) |
| | | } |
| | | return components.map { "\($0)=\($1)" }.joined(separator: "&") |
| | | } |
| | | } |
| | | |
| | | // MARK: - |
| | | |
| | | /// Uses `JSONSerialization` to create a JSON representation of the parameters object, which is set as the body of the |
| | | /// request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`. |
| | | public struct JSONEncoding: ParameterEncoding { |
| | | public enum Error: Swift.Error { |
| | | case invalidJSONObject |
| | | } |
| | | |
| | | // MARK: Properties |
| | | |
| | | /// Returns a `JSONEncoding` instance with default writing options. |
| | | public static var `default`: JSONEncoding { JSONEncoding() } |
| | | |
| | | /// Returns a `JSONEncoding` instance with `.prettyPrinted` writing options. |
| | | public static var prettyPrinted: JSONEncoding { JSONEncoding(options: .prettyPrinted) } |
| | | |
| | | /// The options for writing the parameters as JSON data. |
| | | public let options: JSONSerialization.WritingOptions |
| | | |
| | | // MARK: Initialization |
| | | |
| | | /// Creates an instance using the specified `WritingOptions`. |
| | | /// |
| | | /// - Parameter options: `JSONSerialization.WritingOptions` to use. |
| | | public init(options: JSONSerialization.WritingOptions = []) { |
| | | self.options = options |
| | | } |
| | | |
| | | // MARK: Encoding |
| | | |
| | | public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { |
| | | var urlRequest = try urlRequest.asURLRequest() |
| | | |
| | | guard let parameters = parameters else { return urlRequest } |
| | | |
| | | guard JSONSerialization.isValidJSONObject(parameters) else { |
| | | throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: Error.invalidJSONObject)) |
| | | } |
| | | |
| | | do { |
| | | let data = try JSONSerialization.data(withJSONObject: parameters, options: options) |
| | | |
| | | if urlRequest.headers["Content-Type"] == nil { |
| | | urlRequest.headers.update(.contentType("application/json")) |
| | | } |
| | | |
| | | urlRequest.httpBody = data |
| | | } catch { |
| | | throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) |
| | | } |
| | | |
| | | return urlRequest |
| | | } |
| | | |
| | | /// Encodes any JSON compatible object into a `URLRequest`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - urlRequest: `URLRequestConvertible` value into which the object will be encoded. |
| | | /// - jsonObject: `Any` value (must be JSON compatible` to be encoded into the `URLRequest`. `nil` by default. |
| | | /// |
| | | /// - Returns: The encoded `URLRequest`. |
| | | /// - Throws: Any `Error` produced during encoding. |
| | | public func encode(_ urlRequest: URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest { |
| | | var urlRequest = try urlRequest.asURLRequest() |
| | | |
| | | guard let jsonObject = jsonObject else { return urlRequest } |
| | | |
| | | guard JSONSerialization.isValidJSONObject(jsonObject) else { |
| | | throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: Error.invalidJSONObject)) |
| | | } |
| | | |
| | | do { |
| | | let data = try JSONSerialization.data(withJSONObject: jsonObject, options: options) |
| | | |
| | | if urlRequest.headers["Content-Type"] == nil { |
| | | urlRequest.headers.update(.contentType("application/json")) |
| | | } |
| | | |
| | | urlRequest.httpBody = data |
| | | } catch { |
| | | throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) |
| | | } |
| | | |
| | | return urlRequest |
| | | } |
| | | } |
| | | |
| | | extension JSONEncoding.Error { |
| | | public var localizedDescription: String { |
| | | """ |
| | | Invalid JSON object provided for parameter or object encoding. \ |
| | | This is most likely due to a value which can't be represented in Objective-C. |
| | | """ |
| | | } |
| | | } |
| | | |
| | | // MARK: - |
| | | |
| | | extension NSNumber { |
| | | fileprivate var isBool: Bool { |
| | | // Use Obj-C type encoding to check whether the underlying type is a `Bool`, as it's guaranteed as part of |
| | | // swift-corelibs-foundation, per [this discussion on the Swift forums](https://forums.swift.org/t/alamofire-on-linux-possible-but-not-release-ready/34553/22). |
| | | String(cString: objCType) == "c" |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // Protected.swift |
| | | // |
| | | // Copyright (c) 2014-2020 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | private protocol Lock { |
| | | func lock() |
| | | func unlock() |
| | | } |
| | | |
| | | extension Lock { |
| | | /// Executes a closure returning a value while acquiring the lock. |
| | | /// |
| | | /// - Parameter closure: The closure to run. |
| | | /// |
| | | /// - Returns: The value the closure generated. |
| | | func around<T>(_ closure: () throws -> T) rethrows -> T { |
| | | lock(); defer { unlock() } |
| | | return try closure() |
| | | } |
| | | |
| | | /// Execute a closure while acquiring the lock. |
| | | /// |
| | | /// - Parameter closure: The closure to run. |
| | | func around(_ closure: () throws -> Void) rethrows { |
| | | lock(); defer { unlock() } |
| | | try closure() |
| | | } |
| | | } |
| | | |
| | | #if canImport(Darwin) |
| | | /// An `os_unfair_lock` wrapper. |
| | | final class UnfairLock: Lock { |
| | | private let unfairLock: os_unfair_lock_t |
| | | |
| | | init() { |
| | | unfairLock = .allocate(capacity: 1) |
| | | unfairLock.initialize(to: os_unfair_lock()) |
| | | } |
| | | |
| | | deinit { |
| | | unfairLock.deinitialize(count: 1) |
| | | unfairLock.deallocate() |
| | | } |
| | | |
| | | fileprivate func lock() { |
| | | os_unfair_lock_lock(unfairLock) |
| | | } |
| | | |
| | | fileprivate func unlock() { |
| | | os_unfair_lock_unlock(unfairLock) |
| | | } |
| | | } |
| | | |
| | | #elseif canImport(Foundation) |
| | | extension NSLock: Lock {} |
| | | #else |
| | | #error("This platform needs a Lock-conforming type without Foundation.") |
| | | #endif |
| | | |
| | | /// A thread-safe wrapper around a value. |
| | | @dynamicMemberLookup |
| | | final class Protected<Value> { |
| | | #if canImport(Darwin) |
| | | private let lock = UnfairLock() |
| | | #elseif canImport(Foundation) |
| | | private let lock = NSLock() |
| | | #else |
| | | #error("This platform needs a Lock-conforming type without Foundation.") |
| | | #endif |
| | | private var value: Value |
| | | |
| | | init(_ value: Value) { |
| | | self.value = value |
| | | } |
| | | |
| | | /// Synchronously read or transform the contained value. |
| | | /// |
| | | /// - Parameter closure: The closure to execute. |
| | | /// |
| | | /// - Returns: The return value of the closure passed. |
| | | func read<U>(_ closure: (Value) throws -> U) rethrows -> U { |
| | | try lock.around { try closure(self.value) } |
| | | } |
| | | |
| | | /// Synchronously modify the protected value. |
| | | /// |
| | | /// - Parameter closure: The closure to execute. |
| | | /// |
| | | /// - Returns: The modified value. |
| | | @discardableResult |
| | | func write<U>(_ closure: (inout Value) throws -> U) rethrows -> U { |
| | | try lock.around { try closure(&self.value) } |
| | | } |
| | | |
| | | /// Synchronously update the protected value. |
| | | /// |
| | | /// - Parameter value: The `Value`. |
| | | func write(_ value: Value) { |
| | | write { $0 = value } |
| | | } |
| | | |
| | | subscript<Property>(dynamicMember keyPath: WritableKeyPath<Value, Property>) -> Property { |
| | | get { lock.around { value[keyPath: keyPath] } } |
| | | set { lock.around { value[keyPath: keyPath] = newValue } } |
| | | } |
| | | |
| | | subscript<Property>(dynamicMember keyPath: KeyPath<Value, Property>) -> Property { |
| | | lock.around { value[keyPath: keyPath] } |
| | | } |
| | | } |
| | | |
| | | extension Protected where Value == Request.MutableState { |
| | | /// Attempts to transition to the passed `State`. |
| | | /// |
| | | /// - Parameter state: The `State` to attempt transition to. |
| | | /// |
| | | /// - Returns: Whether the transition occurred. |
| | | func attemptToTransitionTo(_ state: Request.State) -> Bool { |
| | | lock.around { |
| | | guard value.state.canTransitionTo(state) else { return false } |
| | | |
| | | value.state = state |
| | | |
| | | return true |
| | | } |
| | | } |
| | | |
| | | /// Perform a closure while locked with the provided `Request.State`. |
| | | /// |
| | | /// - Parameter perform: The closure to perform while locked. |
| | | func withState(perform: (Request.State) -> Void) { |
| | | lock.around { perform(value.state) } |
| | | } |
| | | } |
| | | |
| | | extension Protected: Equatable where Value: Equatable { |
| | | static func ==(lhs: Protected<Value>, rhs: Protected<Value>) -> Bool { |
| | | lhs.read { left in rhs.read { right in left == right }} |
| | | } |
| | | } |
| | | |
| | | extension Protected: Hashable where Value: Hashable { |
| | | func hash(into hasher: inout Hasher) { |
| | | read { hasher.combine($0) } |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // RedirectHandler.swift |
| | | // |
| | | // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | /// A type that handles how an HTTP redirect response from a remote server should be redirected to the new request. |
| | | public protocol RedirectHandler { |
| | | /// Determines how the HTTP redirect response should be redirected to the new request. |
| | | /// |
| | | /// The `completion` closure should be passed one of three possible options: |
| | | /// |
| | | /// 1. The new request specified by the redirect (this is the most common use case). |
| | | /// 2. A modified version of the new request (you may want to route it somewhere else). |
| | | /// 3. A `nil` value to deny the redirect request and return the body of the redirect response. |
| | | /// |
| | | /// - Parameters: |
| | | /// - task: The `URLSessionTask` whose request resulted in a redirect. |
| | | /// - request: The `URLRequest` to the new location specified by the redirect response. |
| | | /// - response: The `HTTPURLResponse` containing the server's response to the original request. |
| | | /// - completion: The closure to execute containing the new `URLRequest`, a modified `URLRequest`, or `nil`. |
| | | func task(_ task: URLSessionTask, |
| | | willBeRedirectedTo request: URLRequest, |
| | | for response: HTTPURLResponse, |
| | | completion: @escaping (URLRequest?) -> Void) |
| | | } |
| | | |
| | | // MARK: - |
| | | |
| | | /// `Redirector` is a convenience `RedirectHandler` making it easy to follow, not follow, or modify a redirect. |
| | | public struct Redirector { |
| | | /// Defines the behavior of the `Redirector` type. |
| | | public enum Behavior { |
| | | /// Follow the redirect as defined in the response. |
| | | case follow |
| | | /// Do not follow the redirect defined in the response. |
| | | case doNotFollow |
| | | /// Modify the redirect request defined in the response. |
| | | case modify((URLSessionTask, URLRequest, HTTPURLResponse) -> URLRequest?) |
| | | } |
| | | |
| | | /// Returns a `Redirector` with a `.follow` `Behavior`. |
| | | public static let follow = Redirector(behavior: .follow) |
| | | /// Returns a `Redirector` with a `.doNotFollow` `Behavior`. |
| | | public static let doNotFollow = Redirector(behavior: .doNotFollow) |
| | | |
| | | /// The `Behavior` of the `Redirector`. |
| | | public let behavior: Behavior |
| | | |
| | | /// Creates a `Redirector` instance from the `Behavior`. |
| | | /// |
| | | /// - Parameter behavior: The `Behavior`. |
| | | public init(behavior: Behavior) { |
| | | self.behavior = behavior |
| | | } |
| | | } |
| | | |
| | | // MARK: - |
| | | |
| | | extension Redirector: RedirectHandler { |
| | | public func task(_ task: URLSessionTask, |
| | | willBeRedirectedTo request: URLRequest, |
| | | for response: HTTPURLResponse, |
| | | completion: @escaping (URLRequest?) -> Void) { |
| | | switch behavior { |
| | | case .follow: |
| | | completion(request) |
| | | case .doNotFollow: |
| | | completion(nil) |
| | | case let .modify(closure): |
| | | let request = closure(task, request, response) |
| | | completion(request) |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension RedirectHandler where Self == Redirector { |
| | | /// Provides a `Redirector` which follows redirects. Equivalent to `Redirector.follow`. |
| | | public static var follow: Redirector { .follow } |
| | | |
| | | /// Provides a `Redirector` which does not follow redirects. Equivalent to `Redirector.doNotFollow`. |
| | | public static var doNotFollow: Redirector { .doNotFollow } |
| | | |
| | | /// Creates a `Redirector` which modifies the redirected `URLRequest` using the provided closure. |
| | | /// |
| | | /// - Parameter closure: Closure used to modify the redirect. |
| | | /// - Returns: The `Redirector`. |
| | | public static func modify(using closure: @escaping (URLSessionTask, URLRequest, HTTPURLResponse) -> URLRequest?) -> Redirector { |
| | | Redirector(behavior: .modify(closure)) |
| | | } |
| | | } |
New file |
| | |
| | | // |
| | | // Request.swift |
| | | // |
| | | // Copyright (c) 2014-2020 Alamofire Software Foundation (http://alamofire.org/) |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | import Foundation |
| | | |
| | | /// `Request` is the common superclass of all Alamofire request types and provides common state, delegate, and callback |
| | | /// handling. |
| | | public class Request { |
| | | /// State of the `Request`, with managed transitions between states set when calling `resume()`, `suspend()`, or |
| | | /// `cancel()` on the `Request`. |
| | | public enum State { |
| | | /// Initial state of the `Request`. |
| | | case initialized |
| | | /// `State` set when `resume()` is called. Any tasks created for the `Request` will have `resume()` called on |
| | | /// them in this state. |
| | | case resumed |
| | | /// `State` set when `suspend()` is called. Any tasks created for the `Request` will have `suspend()` called on |
| | | /// them in this state. |
| | | case suspended |
| | | /// `State` set when `cancel()` is called. Any tasks created for the `Request` will have `cancel()` called on |
| | | /// them. Unlike `resumed` or `suspended`, once in the `cancelled` state, the `Request` can no longer transition |
| | | /// to any other state. |
| | | case cancelled |
| | | /// `State` set when all response serialization completion closures have been cleared on the `Request` and |
| | | /// enqueued on their respective queues. |
| | | case finished |
| | | |
| | | /// Determines whether `self` can be transitioned to the provided `State`. |
| | | func canTransitionTo(_ state: State) -> Bool { |
| | | switch (self, state) { |
| | | case (.initialized, _): |
| | | return true |
| | | case (_, .initialized), (.cancelled, _), (.finished, _): |
| | | return false |
| | | case (.resumed, .cancelled), (.suspended, .cancelled), (.resumed, .suspended), (.suspended, .resumed): |
| | | return true |
| | | case (.suspended, .suspended), (.resumed, .resumed): |
| | | return false |
| | | case (_, .finished): |
| | | return true |
| | | } |
| | | } |
| | | } |
| | | |
| | | // MARK: - Initial State |
| | | |
| | | /// `UUID` providing a unique identifier for the `Request`, used in the `Hashable` and `Equatable` conformances. |
| | | public let id: UUID |
| | | /// The serial queue for all internal async actions. |
| | | public let underlyingQueue: DispatchQueue |
| | | /// The queue used for all serialization actions. By default it's a serial queue that targets `underlyingQueue`. |
| | | public let serializationQueue: DispatchQueue |
| | | /// `EventMonitor` used for event callbacks. |
| | | public let eventMonitor: EventMonitor? |
| | | /// The `Request`'s interceptor. |
| | | public let interceptor: RequestInterceptor? |
| | | /// The `Request`'s delegate. |
| | | public private(set) weak var delegate: RequestDelegate? |
| | | |
| | | // MARK: - Mutable State |
| | | |
| | | /// Type encapsulating all mutable state that may need to be accessed from anything other than the `underlyingQueue`. |
| | | struct MutableState { |
| | | /// State of the `Request`. |
| | | var state: State = .initialized |
| | | /// `ProgressHandler` and `DispatchQueue` provided for upload progress callbacks. |
| | | var uploadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? |
| | | /// `ProgressHandler` and `DispatchQueue` provided for download progress callbacks. |
| | | var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? |
| | | /// `RedirectHandler` provided for to handle request redirection. |
| | | var redirectHandler: RedirectHandler? |
| | | /// `CachedResponseHandler` provided to handle response caching. |
| | | var cachedResponseHandler: CachedResponseHandler? |
| | | /// Queue and closure called when the `Request` is able to create a cURL description of itself. |
| | | var cURLHandler: (queue: DispatchQueue, handler: (String) -> Void)? |
| | | /// Queue and closure called when the `Request` creates a `URLRequest`. |
| | | var urlRequestHandler: (queue: DispatchQueue, handler: (URLRequest) -> Void)? |
| | | /// Queue and closure called when the `Request` creates a `URLSessionTask`. |
| | | var urlSessionTaskHandler: (queue: DispatchQueue, handler: (URLSessionTask) -> Void)? |
| | | /// Response serialization closures that handle response parsing. |
| | | var responseSerializers: [() -> Void] = [] |
| | | /// Response serialization completion closures executed once all response serializers are complete. |
| | | var responseSerializerCompletions: [() -> Void] = [] |
| | | /// Whether response serializer processing is finished. |
| | | var responseSerializerProcessingFinished = false |
| | | /// `URLCredential` used for authentication challenges. |
| | | var credential: URLCredential? |
| | | /// All `URLRequest`s created by Alamofire on behalf of the `Request`. |
| | | var requests: [URLRequest] = [] |
| | | /// All `URLSessionTask`s created by Alamofire on behalf of the `Request`. |
| | | var tasks: [URLSessionTask] = [] |
| | | /// All `URLSessionTaskMetrics` values gathered by Alamofire on behalf of the `Request`. Should correspond |
| | | /// exactly the the `tasks` created. |
| | | var metrics: [URLSessionTaskMetrics] = [] |
| | | /// Number of times any retriers provided retried the `Request`. |
| | | var retryCount = 0 |
| | | /// Final `AFError` for the `Request`, whether from various internal Alamofire calls or as a result of a `task`. |
| | | var error: AFError? |
| | | /// Whether the instance has had `finish()` called and is running the serializers. Should be replaced with a |
| | | /// representation in the state machine in the future. |
| | | var isFinishing = false |
| | | /// Actions to run when requests are finished. Use for concurrency support. |
| | | var finishHandlers: [() -> Void] = [] |
| | | } |
| | | |
| | | /// Protected `MutableState` value that provides thread-safe access to state values. |
| | | fileprivate let mutableState = Protected(MutableState()) |
| | | |
| | | /// `State` of the `Request`. |
| | | public var state: State { mutableState.state } |
| | | /// Returns whether `state` is `.initialized`. |
| | | public var isInitialized: Bool { state == .initialized } |
| | | /// Returns whether `state is `.resumed`. |
| | | public var isResumed: Bool { state == .resumed } |
| | | /// Returns whether `state` is `.suspended`. |
| | | public var isSuspended: Bool { state == .suspended } |
| | | /// Returns whether `state` is `.cancelled`. |
| | | public var isCancelled: Bool { state == .cancelled } |
| | | /// Returns whether `state` is `.finished`. |
| | | public var isFinished: Bool { state == .finished } |
| | | |
| | | // MARK: Progress |
| | | |
| | | /// Closure type executed when monitoring the upload or download progress of a request. |
| | | public typealias ProgressHandler = (Progress) -> Void |
| | | |
| | | /// `Progress` of the upload of the body of the executed `URLRequest`. Reset to `0` if the `Request` is retried. |
| | | public let uploadProgress = Progress(totalUnitCount: 0) |
| | | /// `Progress` of the download of any response data. Reset to `0` if the `Request` is retried. |
| | | public let downloadProgress = Progress(totalUnitCount: 0) |
| | | /// `ProgressHandler` called when `uploadProgress` is updated, on the provided `DispatchQueue`. |
| | | private var uploadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? { |
| | | get { mutableState.uploadProgressHandler } |
| | | set { mutableState.uploadProgressHandler = newValue } |
| | | } |
| | | |
| | | /// `ProgressHandler` called when `downloadProgress` is updated, on the provided `DispatchQueue`. |
| | | fileprivate var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? { |
| | | get { mutableState.downloadProgressHandler } |
| | | set { mutableState.downloadProgressHandler = newValue } |
| | | } |
| | | |
| | | // MARK: Redirect Handling |
| | | |
| | | /// `RedirectHandler` set on the instance. |
| | | public private(set) var redirectHandler: RedirectHandler? { |
| | | get { mutableState.redirectHandler } |
| | | set { mutableState.redirectHandler = newValue } |
| | | } |
| | | |
| | | // MARK: Cached Response Handling |
| | | |
| | | /// `CachedResponseHandler` set on the instance. |
| | | public private(set) var cachedResponseHandler: CachedResponseHandler? { |
| | | get { mutableState.cachedResponseHandler } |
| | | set { mutableState.cachedResponseHandler = newValue } |
| | | } |
| | | |
| | | // MARK: URLCredential |
| | | |
| | | /// `URLCredential` used for authentication challenges. Created by calling one of the `authenticate` methods. |
| | | public private(set) var credential: URLCredential? { |
| | | get { mutableState.credential } |
| | | set { mutableState.credential = newValue } |
| | | } |
| | | |
| | | // MARK: Validators |
| | | |
| | | /// `Validator` callback closures that store the validation calls enqueued. |
| | | fileprivate let validators = Protected<[() -> Void]>([]) |
| | | |
| | | // MARK: URLRequests |
| | | |
| | | /// All `URLRequests` created on behalf of the `Request`, including original and adapted requests. |
| | | public var requests: [URLRequest] { mutableState.requests } |
| | | /// First `URLRequest` created on behalf of the `Request`. May not be the first one actually executed. |
| | | public var firstRequest: URLRequest? { requests.first } |
| | | /// Last `URLRequest` created on behalf of the `Request`. |
| | | public var lastRequest: URLRequest? { requests.last } |
| | | /// Current `URLRequest` created on behalf of the `Request`. |
| | | public var request: URLRequest? { lastRequest } |
| | | |
| | | /// `URLRequest`s from all of the `URLSessionTask`s executed on behalf of the `Request`. May be different from |
| | | /// `requests` due to `URLSession` manipulation. |
| | | public var performedRequests: [URLRequest] { mutableState.read { $0.tasks.compactMap(\.currentRequest) } } |
| | | |
| | | // MARK: HTTPURLResponse |
| | | |
| | | /// `HTTPURLResponse` received from the server, if any. If the `Request` was retried, this is the response of the |
| | | /// last `URLSessionTask`. |
| | | public var response: HTTPURLResponse? { lastTask?.response as? HTTPURLResponse } |
| | | |
| | | // MARK: Tasks |
| | | |
| | | /// All `URLSessionTask`s created on behalf of the `Request`. |
| | | public var tasks: [URLSessionTask] { mutableState.tasks } |
| | | /// First `URLSessionTask` created on behalf of the `Request`. |
| | | public var firstTask: URLSessionTask? { tasks.first } |
| | | /// Last `URLSessionTask` created on behalf of the `Request`. |
| | | public var lastTask: URLSessionTask? { tasks.last } |
| | | /// Current `URLSessionTask` created on behalf of the `Request`. |
| | | public var task: URLSessionTask? { lastTask } |
| | | |
| | | // MARK: Metrics |
| | | |
| | | /// All `URLSessionTaskMetrics` gathered on behalf of the `Request`. Should correspond to the `tasks` created. |
| | | public var allMetrics: [URLSessionTaskMetrics] { mutableState.metrics } |
| | | /// First `URLSessionTaskMetrics` gathered on behalf of the `Request`. |
| | | public var firstMetrics: URLSessionTaskMetrics? { allMetrics.first } |
| | | /// Last `URLSessionTaskMetrics` gathered on behalf of the `Request`. |
| | | public var lastMetrics: URLSessionTaskMetrics? { allMetrics.last } |
| | | /// Current `URLSessionTaskMetrics` gathered on behalf of the `Request`. |
| | | public var metrics: URLSessionTaskMetrics? { lastMetrics } |
| | | |
| | | // MARK: Retry Count |
| | | |
| | | /// Number of times the `Request` has been retried. |
| | | public var retryCount: Int { mutableState.retryCount } |
| | | |
| | | // MARK: Error |
| | | |
| | | /// `Error` returned from Alamofire internally, from the network request directly, or any validators executed. |
| | | public fileprivate(set) var error: AFError? { |
| | | get { mutableState.error } |
| | | set { mutableState.error = newValue } |
| | | } |
| | | |
| | | /// Default initializer for the `Request` superclass. |
| | | /// |
| | | /// - Parameters: |
| | | /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. |
| | | /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. |
| | | /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets |
| | | /// `underlyingQueue`, but can be passed another queue from a `Session`. |
| | | /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. |
| | | /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. |
| | | /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. |
| | | init(id: UUID = UUID(), |
| | | underlyingQueue: DispatchQueue, |
| | | serializationQueue: DispatchQueue, |
| | | eventMonitor: EventMonitor?, |
| | | interceptor: RequestInterceptor?, |
| | | delegate: RequestDelegate) { |
| | | self.id = id |
| | | self.underlyingQueue = underlyingQueue |
| | | self.serializationQueue = serializationQueue |
| | | self.eventMonitor = eventMonitor |
| | | self.interceptor = interceptor |
| | | self.delegate = delegate |
| | | } |
| | | |
| | | // MARK: - Internal Event API |
| | | |
| | | // All API must be called from underlyingQueue. |
| | | |
| | | /// Called when an initial `URLRequest` has been created on behalf of the instance. If a `RequestAdapter` is active, |
| | | /// the `URLRequest` will be adapted before being issued. |
| | | /// |
| | | /// - Parameter request: The `URLRequest` created. |
| | | func didCreateInitialURLRequest(_ request: URLRequest) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | mutableState.write { $0.requests.append(request) } |
| | | |
| | | eventMonitor?.request(self, didCreateInitialURLRequest: request) |
| | | } |
| | | |
| | | /// Called when initial `URLRequest` creation has failed, typically through a `URLRequestConvertible`. |
| | | /// |
| | | /// - Note: Triggers retry. |
| | | /// |
| | | /// - Parameter error: `AFError` thrown from the failed creation. |
| | | func didFailToCreateURLRequest(with error: AFError) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | self.error = error |
| | | |
| | | eventMonitor?.request(self, didFailToCreateURLRequestWithError: error) |
| | | |
| | | callCURLHandlerIfNecessary() |
| | | |
| | | retryOrFinish(error: error) |
| | | } |
| | | |
| | | /// Called when a `RequestAdapter` has successfully adapted a `URLRequest`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - initialRequest: The `URLRequest` that was adapted. |
| | | /// - adaptedRequest: The `URLRequest` returned by the `RequestAdapter`. |
| | | func didAdaptInitialRequest(_ initialRequest: URLRequest, to adaptedRequest: URLRequest) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | mutableState.write { $0.requests.append(adaptedRequest) } |
| | | |
| | | eventMonitor?.request(self, didAdaptInitialRequest: initialRequest, to: adaptedRequest) |
| | | } |
| | | |
| | | /// Called when a `RequestAdapter` fails to adapt a `URLRequest`. |
| | | /// |
| | | /// - Note: Triggers retry. |
| | | /// |
| | | /// - Parameters: |
| | | /// - request: The `URLRequest` the adapter was called with. |
| | | /// - error: The `AFError` returned by the `RequestAdapter`. |
| | | func didFailToAdaptURLRequest(_ request: URLRequest, withError error: AFError) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | self.error = error |
| | | |
| | | eventMonitor?.request(self, didFailToAdaptURLRequest: request, withError: error) |
| | | |
| | | callCURLHandlerIfNecessary() |
| | | |
| | | retryOrFinish(error: error) |
| | | } |
| | | |
| | | /// Final `URLRequest` has been created for the instance. |
| | | /// |
| | | /// - Parameter request: The `URLRequest` created. |
| | | func didCreateURLRequest(_ request: URLRequest) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | mutableState.read { state in |
| | | state.urlRequestHandler?.queue.async { state.urlRequestHandler?.handler(request) } |
| | | } |
| | | |
| | | eventMonitor?.request(self, didCreateURLRequest: request) |
| | | |
| | | callCURLHandlerIfNecessary() |
| | | } |
| | | |
| | | /// Asynchronously calls any stored `cURLHandler` and then removes it from `mutableState`. |
| | | private func callCURLHandlerIfNecessary() { |
| | | mutableState.write { mutableState in |
| | | guard let cURLHandler = mutableState.cURLHandler else { return } |
| | | |
| | | cURLHandler.queue.async { cURLHandler.handler(self.cURLDescription()) } |
| | | |
| | | mutableState.cURLHandler = nil |
| | | } |
| | | } |
| | | |
| | | /// Called when a `URLSessionTask` is created on behalf of the instance. |
| | | /// |
| | | /// - Parameter task: The `URLSessionTask` created. |
| | | func didCreateTask(_ task: URLSessionTask) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | mutableState.write { state in |
| | | state.tasks.append(task) |
| | | |
| | | guard let urlSessionTaskHandler = state.urlSessionTaskHandler else { return } |
| | | |
| | | urlSessionTaskHandler.queue.async { urlSessionTaskHandler.handler(task) } |
| | | } |
| | | |
| | | eventMonitor?.request(self, didCreateTask: task) |
| | | } |
| | | |
| | | /// Called when resumption is completed. |
| | | func didResume() { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | eventMonitor?.requestDidResume(self) |
| | | } |
| | | |
| | | /// Called when a `URLSessionTask` is resumed on behalf of the instance. |
| | | /// |
| | | /// - Parameter task: The `URLSessionTask` resumed. |
| | | func didResumeTask(_ task: URLSessionTask) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | eventMonitor?.request(self, didResumeTask: task) |
| | | } |
| | | |
| | | /// Called when suspension is completed. |
| | | func didSuspend() { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | eventMonitor?.requestDidSuspend(self) |
| | | } |
| | | |
| | | /// Called when a `URLSessionTask` is suspended on behalf of the instance. |
| | | /// |
| | | /// - Parameter task: The `URLSessionTask` suspended. |
| | | func didSuspendTask(_ task: URLSessionTask) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | eventMonitor?.request(self, didSuspendTask: task) |
| | | } |
| | | |
| | | /// Called when cancellation is completed, sets `error` to `AFError.explicitlyCancelled`. |
| | | func didCancel() { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | mutableState.write { mutableState in |
| | | mutableState.error = mutableState.error ?? AFError.explicitlyCancelled |
| | | } |
| | | |
| | | eventMonitor?.requestDidCancel(self) |
| | | } |
| | | |
| | | /// Called when a `URLSessionTask` is cancelled on behalf of the instance. |
| | | /// |
| | | /// - Parameter task: The `URLSessionTask` cancelled. |
| | | func didCancelTask(_ task: URLSessionTask) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | eventMonitor?.request(self, didCancelTask: task) |
| | | } |
| | | |
| | | /// Called when a `URLSessionTaskMetrics` value is gathered on behalf of the instance. |
| | | /// |
| | | /// - Parameter metrics: The `URLSessionTaskMetrics` gathered. |
| | | func didGatherMetrics(_ metrics: URLSessionTaskMetrics) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | mutableState.write { $0.metrics.append(metrics) } |
| | | |
| | | eventMonitor?.request(self, didGatherMetrics: metrics) |
| | | } |
| | | |
| | | /// Called when a `URLSessionTask` fails before it is finished, typically during certificate pinning. |
| | | /// |
| | | /// - Parameters: |
| | | /// - task: The `URLSessionTask` which failed. |
| | | /// - error: The early failure `AFError`. |
| | | func didFailTask(_ task: URLSessionTask, earlyWithError error: AFError) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | self.error = error |
| | | |
| | | // Task will still complete, so didCompleteTask(_:with:) will handle retry. |
| | | eventMonitor?.request(self, didFailTask: task, earlyWithError: error) |
| | | } |
| | | |
| | | /// Called when a `URLSessionTask` completes. All tasks will eventually call this method. |
| | | /// |
| | | /// - Note: Response validation is synchronously triggered in this step. |
| | | /// |
| | | /// - Parameters: |
| | | /// - task: The `URLSessionTask` which completed. |
| | | /// - error: The `AFError` `task` may have completed with. If `error` has already been set on the instance, this |
| | | /// value is ignored. |
| | | func didCompleteTask(_ task: URLSessionTask, with error: AFError?) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | self.error = self.error ?? error |
| | | |
| | | let validators = validators.read { $0 } |
| | | validators.forEach { $0() } |
| | | |
| | | eventMonitor?.request(self, didCompleteTask: task, with: error) |
| | | |
| | | retryOrFinish(error: self.error) |
| | | } |
| | | |
| | | /// Called when the `RequestDelegate` is going to retry this `Request`. Calls `reset()`. |
| | | func prepareForRetry() { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | mutableState.write { $0.retryCount += 1 } |
| | | |
| | | reset() |
| | | |
| | | eventMonitor?.requestIsRetrying(self) |
| | | } |
| | | |
| | | /// Called to determine whether retry will be triggered for the particular error, or whether the instance should |
| | | /// call `finish()`. |
| | | /// |
| | | /// - Parameter error: The possible `AFError` which may trigger retry. |
| | | func retryOrFinish(error: AFError?) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | guard !isCancelled, let error = error, let delegate = delegate else { finish(); return } |
| | | |
| | | delegate.retryResult(for: self, dueTo: error) { retryResult in |
| | | switch retryResult { |
| | | case .doNotRetry: |
| | | self.finish() |
| | | case let .doNotRetryWithError(retryError): |
| | | self.finish(error: retryError.asAFError(orFailWith: "Received retryError was not already AFError")) |
| | | case .retry, .retryWithDelay: |
| | | delegate.retryRequest(self, withDelay: retryResult.delay) |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// Finishes this `Request` and starts the response serializers. |
| | | /// |
| | | /// - Parameter error: The possible `Error` with which the instance will finish. |
| | | func finish(error: AFError? = nil) { |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | guard !mutableState.isFinishing else { return } |
| | | |
| | | mutableState.isFinishing = true |
| | | |
| | | if let error = error { self.error = error } |
| | | |
| | | // Start response handlers |
| | | processNextResponseSerializer() |
| | | |
| | | eventMonitor?.requestDidFinish(self) |
| | | } |
| | | |
| | | /// Appends the response serialization closure to the instance. |
| | | /// |
| | | /// - Note: This method will also `resume` the instance if `delegate.startImmediately` returns `true`. |
| | | /// |
| | | /// - Parameter closure: The closure containing the response serialization call. |
| | | func appendResponseSerializer(_ closure: @escaping () -> Void) { |
| | | mutableState.write { mutableState in |
| | | mutableState.responseSerializers.append(closure) |
| | | |
| | | if mutableState.state == .finished { |
| | | mutableState.state = .resumed |
| | | } |
| | | |
| | | if mutableState.responseSerializerProcessingFinished { |
| | | underlyingQueue.async { self.processNextResponseSerializer() } |
| | | } |
| | | |
| | | if mutableState.state.canTransitionTo(.resumed) { |
| | | underlyingQueue.async { if self.delegate?.startImmediately == true { self.resume() } } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// Returns the next response serializer closure to execute if there's one left. |
| | | /// |
| | | /// - Returns: The next response serialization closure, if there is one. |
| | | func nextResponseSerializer() -> (() -> Void)? { |
| | | var responseSerializer: (() -> Void)? |
| | | |
| | | mutableState.write { mutableState in |
| | | let responseSerializerIndex = mutableState.responseSerializerCompletions.count |
| | | |
| | | if responseSerializerIndex < mutableState.responseSerializers.count { |
| | | responseSerializer = mutableState.responseSerializers[responseSerializerIndex] |
| | | } |
| | | } |
| | | |
| | | return responseSerializer |
| | | } |
| | | |
| | | /// Processes the next response serializer and calls all completions if response serialization is complete. |
| | | func processNextResponseSerializer() { |
| | | guard let responseSerializer = nextResponseSerializer() else { |
| | | // Execute all response serializer completions and clear them |
| | | var completions: [() -> Void] = [] |
| | | |
| | | mutableState.write { mutableState in |
| | | completions = mutableState.responseSerializerCompletions |
| | | |
| | | // Clear out all response serializers and response serializer completions in mutable state since the |
| | | // request is complete. It's important to do this prior to calling the completion closures in case |
| | | // the completions call back into the request triggering a re-processing of the response serializers. |
| | | // An example of how this can happen is by calling cancel inside a response completion closure. |
| | | mutableState.responseSerializers.removeAll() |
| | | mutableState.responseSerializerCompletions.removeAll() |
| | | |
| | | if mutableState.state.canTransitionTo(.finished) { |
| | | mutableState.state = .finished |
| | | } |
| | | |
| | | mutableState.responseSerializerProcessingFinished = true |
| | | mutableState.isFinishing = false |
| | | } |
| | | |
| | | completions.forEach { $0() } |
| | | |
| | | // Cleanup the request |
| | | cleanup() |
| | | |
| | | return |
| | | } |
| | | |
| | | serializationQueue.async { responseSerializer() } |
| | | } |
| | | |
| | | /// Notifies the `Request` that the response serializer is complete. |
| | | /// |
| | | /// - Parameter completion: The completion handler provided with the response serializer, called when all serializers |
| | | /// are complete. |
| | | func responseSerializerDidComplete(completion: @escaping () -> Void) { |
| | | mutableState.write { $0.responseSerializerCompletions.append(completion) } |
| | | processNextResponseSerializer() |
| | | } |
| | | |
| | | /// Resets all task and response serializer related state for retry. |
| | | func reset() { |
| | | error = nil |
| | | |
| | | uploadProgress.totalUnitCount = 0 |
| | | uploadProgress.completedUnitCount = 0 |
| | | downloadProgress.totalUnitCount = 0 |
| | | downloadProgress.completedUnitCount = 0 |
| | | |
| | | mutableState.write { state in |
| | | state.isFinishing = false |
| | | state.responseSerializerCompletions = [] |
| | | } |
| | | } |
| | | |
| | | /// Called when updating the upload progress. |
| | | /// |
| | | /// - Parameters: |
| | | /// - totalBytesSent: Total bytes sent so far. |
| | | /// - totalBytesExpectedToSend: Total bytes expected to send. |
| | | func updateUploadProgress(totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { |
| | | uploadProgress.totalUnitCount = totalBytesExpectedToSend |
| | | uploadProgress.completedUnitCount = totalBytesSent |
| | | |
| | | uploadProgressHandler?.queue.async { self.uploadProgressHandler?.handler(self.uploadProgress) } |
| | | } |
| | | |
| | | /// Perform a closure on the current `state` while locked. |
| | | /// |
| | | /// - Parameter perform: The closure to perform. |
| | | func withState(perform: (State) -> Void) { |
| | | mutableState.withState(perform: perform) |
| | | } |
| | | |
| | | // MARK: Task Creation |
| | | |
| | | /// Called when creating a `URLSessionTask` for this `Request`. Subclasses must override. |
| | | /// |
| | | /// - Parameters: |
| | | /// - request: `URLRequest` to use to create the `URLSessionTask`. |
| | | /// - session: `URLSession` which creates the `URLSessionTask`. |
| | | /// |
| | | /// - Returns: The `URLSessionTask` created. |
| | | func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { |
| | | fatalError("Subclasses must override.") |
| | | } |
| | | |
| | | // MARK: - Public API |
| | | |
| | | // These APIs are callable from any queue. |
| | | |
| | | // MARK: State |
| | | |
| | | /// Cancels the instance. Once cancelled, a `Request` can no longer be resumed or suspended. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func cancel() -> Self { |
| | | mutableState.write { mutableState in |
| | | guard mutableState.state.canTransitionTo(.cancelled) else { return } |
| | | |
| | | mutableState.state = .cancelled |
| | | |
| | | underlyingQueue.async { self.didCancel() } |
| | | |
| | | guard let task = mutableState.tasks.last, task.state != .completed else { |
| | | underlyingQueue.async { self.finish() } |
| | | return |
| | | } |
| | | |
| | | // Resume to ensure metrics are gathered. |
| | | task.resume() |
| | | task.cancel() |
| | | underlyingQueue.async { self.didCancelTask(task) } |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | /// Suspends the instance. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func suspend() -> Self { |
| | | mutableState.write { mutableState in |
| | | guard mutableState.state.canTransitionTo(.suspended) else { return } |
| | | |
| | | mutableState.state = .suspended |
| | | |
| | | underlyingQueue.async { self.didSuspend() } |
| | | |
| | | guard let task = mutableState.tasks.last, task.state != .completed else { return } |
| | | |
| | | task.suspend() |
| | | underlyingQueue.async { self.didSuspendTask(task) } |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | /// Resumes the instance. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func resume() -> Self { |
| | | mutableState.write { mutableState in |
| | | guard mutableState.state.canTransitionTo(.resumed) else { return } |
| | | |
| | | mutableState.state = .resumed |
| | | |
| | | underlyingQueue.async { self.didResume() } |
| | | |
| | | guard let task = mutableState.tasks.last, task.state != .completed else { return } |
| | | |
| | | task.resume() |
| | | underlyingQueue.async { self.didResumeTask(task) } |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | // MARK: - Closure API |
| | | |
| | | /// Associates a credential using the provided values with the instance. |
| | | /// |
| | | /// - Parameters: |
| | | /// - username: The username. |
| | | /// - password: The password. |
| | | /// - persistence: The `URLCredential.Persistence` for the created `URLCredential`. `.forSession` by default. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func authenticate(username: String, password: String, persistence: URLCredential.Persistence = .forSession) -> Self { |
| | | let credential = URLCredential(user: username, password: password, persistence: persistence) |
| | | |
| | | return authenticate(with: credential) |
| | | } |
| | | |
| | | /// Associates the provided credential with the instance. |
| | | /// |
| | | /// - Parameter credential: The `URLCredential`. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func authenticate(with credential: URLCredential) -> Self { |
| | | mutableState.credential = credential |
| | | |
| | | return self |
| | | } |
| | | |
| | | /// Sets a closure to be called periodically during the lifecycle of the instance as data is read from the server. |
| | | /// |
| | | /// - Note: Only the last closure provided is used. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: The `DispatchQueue` to execute the closure on. `.main` by default. |
| | | /// - closure: The closure to be executed periodically as data is read from the server. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func downloadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { |
| | | mutableState.downloadProgressHandler = (handler: closure, queue: queue) |
| | | |
| | | return self |
| | | } |
| | | |
| | | /// Sets a closure to be called periodically during the lifecycle of the instance as data is sent to the server. |
| | | /// |
| | | /// - Note: Only the last closure provided is used. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: The `DispatchQueue` to execute the closure on. `.main` by default. |
| | | /// - closure: The closure to be executed periodically as data is sent to the server. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func uploadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { |
| | | mutableState.uploadProgressHandler = (handler: closure, queue: queue) |
| | | |
| | | return self |
| | | } |
| | | |
| | | // MARK: Redirects |
| | | |
| | | /// Sets the redirect handler for the instance which will be used if a redirect response is encountered. |
| | | /// |
| | | /// - Note: Attempting to set the redirect handler more than once is a logic error and will crash. |
| | | /// |
| | | /// - Parameter handler: The `RedirectHandler`. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func redirect(using handler: RedirectHandler) -> Self { |
| | | mutableState.write { mutableState in |
| | | precondition(mutableState.redirectHandler == nil, "Redirect handler has already been set.") |
| | | mutableState.redirectHandler = handler |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | // MARK: Cached Responses |
| | | |
| | | /// Sets the cached response handler for the `Request` which will be used when attempting to cache a response. |
| | | /// |
| | | /// - Note: Attempting to set the cache handler more than once is a logic error and will crash. |
| | | /// |
| | | /// - Parameter handler: The `CachedResponseHandler`. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func cacheResponse(using handler: CachedResponseHandler) -> Self { |
| | | mutableState.write { mutableState in |
| | | precondition(mutableState.cachedResponseHandler == nil, "Cached response handler has already been set.") |
| | | mutableState.cachedResponseHandler = handler |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | // MARK: - Lifetime APIs |
| | | |
| | | /// Sets a handler to be called when the cURL description of the request is available. |
| | | /// |
| | | /// - Note: When waiting for a `Request`'s `URLRequest` to be created, only the last `handler` will be called. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which `handler` will be called. |
| | | /// - handler: Closure to be called when the cURL description is available. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func cURLDescription(on queue: DispatchQueue, calling handler: @escaping (String) -> Void) -> Self { |
| | | mutableState.write { mutableState in |
| | | if mutableState.requests.last != nil { |
| | | queue.async { handler(self.cURLDescription()) } |
| | | } else { |
| | | mutableState.cURLHandler = (queue, handler) |
| | | } |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | /// Sets a handler to be called when the cURL description of the request is available. |
| | | /// |
| | | /// - Note: When waiting for a `Request`'s `URLRequest` to be created, only the last `handler` will be called. |
| | | /// |
| | | /// - Parameter handler: Closure to be called when the cURL description is available. Called on the instance's |
| | | /// `underlyingQueue` by default. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func cURLDescription(calling handler: @escaping (String) -> Void) -> Self { |
| | | cURLDescription(on: underlyingQueue, calling: handler) |
| | | |
| | | return self |
| | | } |
| | | |
| | | /// Sets a closure to called whenever Alamofire creates a `URLRequest` for this instance. |
| | | /// |
| | | /// - Note: This closure will be called multiple times if the instance adapts incoming `URLRequest`s or is retried. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which `handler` will be called. `.main` by default. |
| | | /// - handler: Closure to be called when a `URLRequest` is available. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func onURLRequestCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLRequest) -> Void) -> Self { |
| | | mutableState.write { state in |
| | | if let request = state.requests.last { |
| | | queue.async { handler(request) } |
| | | } |
| | | |
| | | state.urlRequestHandler = (queue, handler) |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | /// Sets a closure to be called whenever the instance creates a `URLSessionTask`. |
| | | /// |
| | | /// - Note: This API should only be used to provide `URLSessionTask`s to existing API, like `NSFileProvider`. It |
| | | /// **SHOULD NOT** be used to interact with tasks directly, as that may be break Alamofire features. |
| | | /// Additionally, this closure may be called multiple times if the instance is retried. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which `handler` will be called. `.main` by default. |
| | | /// - handler: Closure to be called when the `URLSessionTask` is available. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func onURLSessionTaskCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLSessionTask) -> Void) -> Self { |
| | | mutableState.write { state in |
| | | if let task = state.tasks.last { |
| | | queue.async { handler(task) } |
| | | } |
| | | |
| | | state.urlSessionTaskHandler = (queue, handler) |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | // MARK: Cleanup |
| | | |
| | | /// Adds a `finishHandler` closure to be called when the request completes. |
| | | /// |
| | | /// - Parameter closure: Closure to be called when the request finishes. |
| | | func onFinish(perform finishHandler: @escaping () -> Void) { |
| | | guard !isFinished else { finishHandler(); return } |
| | | |
| | | mutableState.write { state in |
| | | state.finishHandlers.append(finishHandler) |
| | | } |
| | | } |
| | | |
| | | /// Final cleanup step executed when the instance finishes response serialization. |
| | | func cleanup() { |
| | | let handlers = mutableState.finishHandlers |
| | | handlers.forEach { $0() } |
| | | mutableState.write { state in |
| | | state.finishHandlers.removeAll() |
| | | } |
| | | |
| | | delegate?.cleanup(after: self) |
| | | } |
| | | } |
| | | |
| | | extension Request { |
| | | /// Type indicating how a `DataRequest` or `DataStreamRequest` should proceed after receiving an `HTTPURLResponse`. |
| | | public enum ResponseDisposition { |
| | | /// Allow the request to continue normally. |
| | | case allow |
| | | /// Cancel the request, similar to calling `cancel()`. |
| | | case cancel |
| | | |
| | | var sessionDisposition: URLSession.ResponseDisposition { |
| | | switch self { |
| | | case .allow: return .allow |
| | | case .cancel: return .cancel |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // MARK: - Protocol Conformances |
| | | |
| | | extension Request: Equatable { |
| | | public static func ==(lhs: Request, rhs: Request) -> Bool { |
| | | lhs.id == rhs.id |
| | | } |
| | | } |
| | | |
| | | extension Request: Hashable { |
| | | public func hash(into hasher: inout Hasher) { |
| | | hasher.combine(id) |
| | | } |
| | | } |
| | | |
| | | extension Request: CustomStringConvertible { |
| | | /// A textual representation of this instance, including the `HTTPMethod` and `URL` if the `URLRequest` has been |
| | | /// created, as well as the response status code, if a response has been received. |
| | | public var description: String { |
| | | guard let request = performedRequests.last ?? lastRequest, |
| | | let url = request.url, |
| | | let method = request.httpMethod else { return "No request created yet." } |
| | | |
| | | let requestDescription = "\(method) \(url.absoluteString)" |
| | | |
| | | return response.map { "\(requestDescription) (\($0.statusCode))" } ?? requestDescription |
| | | } |
| | | } |
| | | |
| | | extension Request { |
| | | /// cURL representation of the instance. |
| | | /// |
| | | /// - Returns: The cURL equivalent of the instance. |
| | | public func cURLDescription() -> String { |
| | | guard |
| | | let request = lastRequest, |
| | | let url = request.url, |
| | | let host = url.host, |
| | | let method = request.httpMethod else { return "$ curl command could not be created" } |
| | | |
| | | var components = ["$ curl -v"] |
| | | |
| | | components.append("-X \(method)") |
| | | |
| | | if let credentialStorage = delegate?.sessionConfiguration.urlCredentialStorage { |
| | | let protectionSpace = URLProtectionSpace(host: host, |
| | | port: url.port ?? 0, |
| | | protocol: url.scheme, |
| | | realm: host, |
| | | authenticationMethod: NSURLAuthenticationMethodHTTPBasic) |
| | | |
| | | if let credentials = credentialStorage.credentials(for: protectionSpace)?.values { |
| | | for credential in credentials { |
| | | guard let user = credential.user, let password = credential.password else { continue } |
| | | components.append("-u \(user):\(password)") |
| | | } |
| | | } else { |
| | | if let credential = credential, let user = credential.user, let password = credential.password { |
| | | components.append("-u \(user):\(password)") |
| | | } |
| | | } |
| | | } |
| | | |
| | | if let configuration = delegate?.sessionConfiguration, configuration.httpShouldSetCookies { |
| | | if |
| | | let cookieStorage = configuration.httpCookieStorage, |
| | | let cookies = cookieStorage.cookies(for: url), !cookies.isEmpty { |
| | | let allCookies = cookies.map { "\($0.name)=\($0.value)" }.joined(separator: ";") |
| | | |
| | | components.append("-b \"\(allCookies)\"") |
| | | } |
| | | } |
| | | |
| | | var headers = HTTPHeaders() |
| | | |
| | | if let sessionHeaders = delegate?.sessionConfiguration.headers { |
| | | for header in sessionHeaders where header.name != "Cookie" { |
| | | headers[header.name] = header.value |
| | | } |
| | | } |
| | | |
| | | for header in request.headers where header.name != "Cookie" { |
| | | headers[header.name] = header.value |
| | | } |
| | | |
| | | for header in headers { |
| | | let escapedValue = header.value.replacingOccurrences(of: "\"", with: "\\\"") |
| | | components.append("-H \"\(header.name): \(escapedValue)\"") |
| | | } |
| | | |
| | | if let httpBodyData = request.httpBody { |
| | | let httpBody = String(decoding: httpBodyData, as: UTF8.self) |
| | | var escapedBody = httpBody.replacingOccurrences(of: "\\\"", with: "\\\\\"") |
| | | escapedBody = escapedBody.replacingOccurrences(of: "\"", with: "\\\"") |
| | | |
| | | components.append("-d \"\(escapedBody)\"") |
| | | } |
| | | |
| | | components.append("\"\(url.absoluteString)\"") |
| | | |
| | | return components.joined(separator: " \\\n\t") |
| | | } |
| | | } |
| | | |
| | | /// Protocol abstraction for `Request`'s communication back to the `SessionDelegate`. |
| | | public protocol RequestDelegate: AnyObject { |
| | | /// `URLSessionConfiguration` used to create the underlying `URLSessionTask`s. |
| | | var sessionConfiguration: URLSessionConfiguration { get } |
| | | |
| | | /// Determines whether the `Request` should automatically call `resume()` when adding the first response handler. |
| | | var startImmediately: Bool { get } |
| | | |
| | | /// Notifies the delegate the `Request` has reached a point where it needs cleanup. |
| | | /// |
| | | /// - Parameter request: The `Request` to cleanup after. |
| | | func cleanup(after request: Request) |
| | | |
| | | /// Asynchronously ask the delegate whether a `Request` will be retried. |
| | | /// |
| | | /// - Parameters: |
| | | /// - request: `Request` which failed. |
| | | /// - error: `Error` which produced the failure. |
| | | /// - completion: Closure taking the `RetryResult` for evaluation. |
| | | func retryResult(for request: Request, dueTo error: AFError, completion: @escaping (RetryResult) -> Void) |
| | | |
| | | /// Asynchronously retry the `Request`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - request: `Request` which will be retried. |
| | | /// - timeDelay: `TimeInterval` after which the retry will be triggered. |
| | | func retryRequest(_ request: Request, withDelay timeDelay: TimeInterval?) |
| | | } |
| | | |
| | | // MARK: - Subclasses |
| | | |
| | | // MARK: - DataRequest |
| | | |
| | | /// `Request` subclass which handles in-memory `Data` download using `URLSessionDataTask`. |
| | | public class DataRequest: Request { |
| | | /// `URLRequestConvertible` value used to create `URLRequest`s for this instance. |
| | | public let convertible: URLRequestConvertible |
| | | /// `Data` read from the server so far. |
| | | public var data: Data? { dataMutableState.data } |
| | | |
| | | private struct DataMutableState { |
| | | var data: Data? |
| | | var httpResponseHandler: (queue: DispatchQueue, |
| | | handler: (_ response: HTTPURLResponse, |
| | | _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void)? |
| | | } |
| | | |
| | | private let dataMutableState = Protected(DataMutableState()) |
| | | |
| | | /// Creates a `DataRequest` using the provided parameters. |
| | | /// |
| | | /// - Parameters: |
| | | /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. |
| | | /// - convertible: `URLRequestConvertible` value used to create `URLRequest`s for this instance. |
| | | /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. |
| | | /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets |
| | | /// `underlyingQueue`, but can be passed another queue from a `Session`. |
| | | /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. |
| | | /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. |
| | | /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. |
| | | init(id: UUID = UUID(), |
| | | convertible: URLRequestConvertible, |
| | | underlyingQueue: DispatchQueue, |
| | | serializationQueue: DispatchQueue, |
| | | eventMonitor: EventMonitor?, |
| | | interceptor: RequestInterceptor?, |
| | | delegate: RequestDelegate) { |
| | | self.convertible = convertible |
| | | |
| | | super.init(id: id, |
| | | underlyingQueue: underlyingQueue, |
| | | serializationQueue: serializationQueue, |
| | | eventMonitor: eventMonitor, |
| | | interceptor: interceptor, |
| | | delegate: delegate) |
| | | } |
| | | |
| | | override func reset() { |
| | | super.reset() |
| | | |
| | | dataMutableState.write { mutableState in |
| | | mutableState.data = nil |
| | | } |
| | | } |
| | | |
| | | /// Called when `Data` is received by this instance. |
| | | /// |
| | | /// - Note: Also calls `updateDownloadProgress`. |
| | | /// |
| | | /// - Parameter data: The `Data` received. |
| | | func didReceive(data: Data) { |
| | | dataMutableState.write { mutableState in |
| | | if mutableState.data == nil { |
| | | mutableState.data = data |
| | | } else { |
| | | mutableState.data?.append(data) |
| | | } |
| | | } |
| | | |
| | | updateDownloadProgress() |
| | | } |
| | | |
| | | func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { |
| | | dataMutableState.read { dataMutableState in |
| | | guard let httpResponseHandler = dataMutableState.httpResponseHandler else { |
| | | underlyingQueue.async { completionHandler(.allow) } |
| | | return |
| | | } |
| | | |
| | | httpResponseHandler.queue.async { |
| | | httpResponseHandler.handler(response) { disposition in |
| | | if disposition == .cancel { |
| | | self.mutableState.write { mutableState in |
| | | mutableState.state = .cancelled |
| | | mutableState.error = mutableState.error ?? AFError.explicitlyCancelled |
| | | } |
| | | } |
| | | |
| | | self.underlyingQueue.async { |
| | | completionHandler(disposition.sessionDisposition) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { |
| | | let copiedRequest = request |
| | | return session.dataTask(with: copiedRequest) |
| | | } |
| | | |
| | | /// Called to update the `downloadProgress` of the instance. |
| | | func updateDownloadProgress() { |
| | | let totalBytesReceived = Int64(data?.count ?? 0) |
| | | let totalBytesExpected = task?.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown |
| | | |
| | | downloadProgress.totalUnitCount = totalBytesExpected |
| | | downloadProgress.completedUnitCount = totalBytesReceived |
| | | |
| | | downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) } |
| | | } |
| | | |
| | | /// Validates the request, using the specified closure. |
| | | /// |
| | | /// - Note: If validation fails, subsequent calls to response handlers will have an associated error. |
| | | /// |
| | | /// - Parameter validation: `Validation` closure used to validate the response. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func validate(_ validation: @escaping Validation) -> Self { |
| | | let validator: () -> Void = { [unowned self] in |
| | | guard error == nil, let response = response else { return } |
| | | |
| | | let result = validation(request, response, data) |
| | | |
| | | if case let .failure(error) = result { self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) } |
| | | |
| | | eventMonitor?.request(self, |
| | | didValidateRequest: request, |
| | | response: response, |
| | | data: data, |
| | | withResult: result) |
| | | } |
| | | |
| | | validators.write { $0.append(validator) } |
| | | |
| | | return self |
| | | } |
| | | |
| | | /// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse` and providing a completion |
| | | /// handler to return a `ResponseDisposition` value. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which the closure will be called. `.main` by default. |
| | | /// - handler: Closure called when the instance produces an `HTTPURLResponse`. The `completionHandler` provided |
| | | /// MUST be called, otherwise the request will never complete. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @_disfavoredOverload |
| | | @discardableResult |
| | | public func onHTTPResponse( |
| | | on queue: DispatchQueue = .main, |
| | | perform handler: @escaping (_ response: HTTPURLResponse, |
| | | _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void |
| | | ) -> Self { |
| | | dataMutableState.write { mutableState in |
| | | mutableState.httpResponseHandler = (queue, handler) |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | /// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which the closure will be called. `.main` by default. |
| | | /// - handler: Closure called when the instance produces an `HTTPURLResponse`. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func onHTTPResponse(on queue: DispatchQueue = .main, |
| | | perform handler: @escaping (HTTPURLResponse) -> Void) -> Self { |
| | | onHTTPResponse(on: queue) { response, completionHandler in |
| | | handler(response) |
| | | completionHandler(.allow) |
| | | } |
| | | |
| | | return self |
| | | } |
| | | } |
| | | |
| | | // MARK: - DataStreamRequest |
| | | |
| | | /// `Request` subclass which streams HTTP response `Data` through a `Handler` closure. |
| | | public final class DataStreamRequest: Request { |
| | | /// Closure type handling `DataStreamRequest.Stream` values. |
| | | public typealias Handler<Success, Failure: Error> = (Stream<Success, Failure>) throws -> Void |
| | | |
| | | /// Type encapsulating an `Event` as it flows through the stream, as well as a `CancellationToken` which can be used |
| | | /// to stop the stream at any time. |
| | | public struct Stream<Success, Failure: Error> { |
| | | /// Latest `Event` from the stream. |
| | | public let event: Event<Success, Failure> |
| | | /// Token used to cancel the stream. |
| | | public let token: CancellationToken |
| | | |
| | | /// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`. |
| | | public func cancel() { |
| | | token.cancel() |
| | | } |
| | | } |
| | | |
| | | /// Type representing an event flowing through the stream. Contains either the `Result` of processing streamed |
| | | /// `Data` or the completion of the stream. |
| | | public enum Event<Success, Failure: Error> { |
| | | /// Output produced every time the instance receives additional `Data`. The associated value contains the |
| | | /// `Result` of processing the incoming `Data`. |
| | | case stream(Result<Success, Failure>) |
| | | /// Output produced when the instance has completed, whether due to stream end, cancellation, or an error. |
| | | /// Associated `Completion` value contains the final state. |
| | | case complete(Completion) |
| | | } |
| | | |
| | | /// Value containing the state of a `DataStreamRequest` when the stream was completed. |
| | | public struct Completion { |
| | | /// Last `URLRequest` issued by the instance. |
| | | public let request: URLRequest? |
| | | /// Last `HTTPURLResponse` received by the instance. |
| | | public let response: HTTPURLResponse? |
| | | /// Last `URLSessionTaskMetrics` produced for the instance. |
| | | public let metrics: URLSessionTaskMetrics? |
| | | /// `AFError` produced for the instance, if any. |
| | | public let error: AFError? |
| | | } |
| | | |
| | | /// Type used to cancel an ongoing stream. |
| | | public struct CancellationToken { |
| | | weak var request: DataStreamRequest? |
| | | |
| | | init(_ request: DataStreamRequest) { |
| | | self.request = request |
| | | } |
| | | |
| | | /// Cancel the ongoing stream by canceling the underlying `DataStreamRequest`. |
| | | public func cancel() { |
| | | request?.cancel() |
| | | } |
| | | } |
| | | |
| | | /// `URLRequestConvertible` value used to create `URLRequest`s for this instance. |
| | | public let convertible: URLRequestConvertible |
| | | /// Whether or not the instance will be cancelled if stream parsing encounters an error. |
| | | public let automaticallyCancelOnStreamError: Bool |
| | | |
| | | /// Internal mutable state specific to this type. |
| | | struct StreamMutableState { |
| | | /// `OutputStream` bound to the `InputStream` produced by `asInputStream`, if it has been called. |
| | | var outputStream: OutputStream? |
| | | /// Stream closures called as `Data` is received. |
| | | var streams: [(_ data: Data) -> Void] = [] |
| | | /// Number of currently executing streams. Used to ensure completions are only fired after all streams are |
| | | /// enqueued. |
| | | var numberOfExecutingStreams = 0 |
| | | /// Completion calls enqueued while streams are still executing. |
| | | var enqueuedCompletionEvents: [() -> Void] = [] |
| | | /// Handler for any `HTTPURLResponse`s received. |
| | | var httpResponseHandler: (queue: DispatchQueue, |
| | | handler: (_ response: HTTPURLResponse, |
| | | _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void)? |
| | | } |
| | | |
| | | let streamMutableState = Protected(StreamMutableState()) |
| | | |
| | | /// Creates a `DataStreamRequest` using the provided parameters. |
| | | /// |
| | | /// - Parameters: |
| | | /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` |
| | | /// by default. |
| | | /// - convertible: `URLRequestConvertible` value used to create `URLRequest`s for this |
| | | /// instance. |
| | | /// - automaticallyCancelOnStreamError: `Bool` indicating whether the instance will be cancelled when an `Error` |
| | | /// is thrown while serializing stream `Data`. |
| | | /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. |
| | | /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default |
| | | /// targets |
| | | /// `underlyingQueue`, but can be passed another queue from a `Session`. |
| | | /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. |
| | | /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. |
| | | /// - delegate: `RequestDelegate` that provides an interface to actions not performed by |
| | | /// the `Request`. |
| | | init(id: UUID = UUID(), |
| | | convertible: URLRequestConvertible, |
| | | automaticallyCancelOnStreamError: Bool, |
| | | underlyingQueue: DispatchQueue, |
| | | serializationQueue: DispatchQueue, |
| | | eventMonitor: EventMonitor?, |
| | | interceptor: RequestInterceptor?, |
| | | delegate: RequestDelegate) { |
| | | self.convertible = convertible |
| | | self.automaticallyCancelOnStreamError = automaticallyCancelOnStreamError |
| | | |
| | | super.init(id: id, |
| | | underlyingQueue: underlyingQueue, |
| | | serializationQueue: serializationQueue, |
| | | eventMonitor: eventMonitor, |
| | | interceptor: interceptor, |
| | | delegate: delegate) |
| | | } |
| | | |
| | | override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { |
| | | let copiedRequest = request |
| | | return session.dataTask(with: copiedRequest) |
| | | } |
| | | |
| | | override func finish(error: AFError? = nil) { |
| | | streamMutableState.write { state in |
| | | state.outputStream?.close() |
| | | } |
| | | |
| | | super.finish(error: error) |
| | | } |
| | | |
| | | func didReceive(data: Data) { |
| | | streamMutableState.write { state in |
| | | #if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation. |
| | | if let stream = state.outputStream { |
| | | underlyingQueue.async { |
| | | var bytes = Array(data) |
| | | stream.write(&bytes, maxLength: bytes.count) |
| | | } |
| | | } |
| | | #endif |
| | | state.numberOfExecutingStreams += state.streams.count |
| | | let localState = state |
| | | underlyingQueue.async { localState.streams.forEach { $0(data) } } |
| | | } |
| | | } |
| | | |
| | | func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { |
| | | streamMutableState.read { dataMutableState in |
| | | guard let httpResponseHandler = dataMutableState.httpResponseHandler else { |
| | | underlyingQueue.async { completionHandler(.allow) } |
| | | return |
| | | } |
| | | |
| | | httpResponseHandler.queue.async { |
| | | httpResponseHandler.handler(response) { disposition in |
| | | if disposition == .cancel { |
| | | self.mutableState.write { mutableState in |
| | | mutableState.state = .cancelled |
| | | mutableState.error = mutableState.error ?? AFError.explicitlyCancelled |
| | | } |
| | | } |
| | | |
| | | self.underlyingQueue.async { |
| | | completionHandler(disposition.sessionDisposition) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// Validates the `URLRequest` and `HTTPURLResponse` received for the instance using the provided `Validation` closure. |
| | | /// |
| | | /// - Parameter validation: `Validation` closure used to validate the request and response. |
| | | /// |
| | | /// - Returns: The `DataStreamRequest`. |
| | | @discardableResult |
| | | public func validate(_ validation: @escaping Validation) -> Self { |
| | | let validator: () -> Void = { [unowned self] in |
| | | guard error == nil, let response = response else { return } |
| | | |
| | | let result = validation(request, response) |
| | | |
| | | if case let .failure(error) = result { |
| | | self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) |
| | | } |
| | | |
| | | eventMonitor?.request(self, |
| | | didValidateRequest: request, |
| | | response: response, |
| | | withResult: result) |
| | | } |
| | | |
| | | validators.write { $0.append(validator) } |
| | | |
| | | return self |
| | | } |
| | | |
| | | #if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation. |
| | | /// Produces an `InputStream` that receives the `Data` received by the instance. |
| | | /// |
| | | /// - Note: The `InputStream` produced by this method must have `open()` called before being able to read `Data`. |
| | | /// Additionally, this method will automatically call `resume()` on the instance, regardless of whether or |
| | | /// not the creating session has `startRequestsImmediately` set to `true`. |
| | | /// |
| | | /// - Parameter bufferSize: Size, in bytes, of the buffer between the `OutputStream` and `InputStream`. |
| | | /// |
| | | /// - Returns: The `InputStream` bound to the internal `OutboundStream`. |
| | | public func asInputStream(bufferSize: Int = 1024) -> InputStream? { |
| | | defer { resume() } |
| | | |
| | | var inputStream: InputStream? |
| | | streamMutableState.write { state in |
| | | Foundation.Stream.getBoundStreams(withBufferSize: bufferSize, |
| | | inputStream: &inputStream, |
| | | outputStream: &state.outputStream) |
| | | state.outputStream?.open() |
| | | } |
| | | |
| | | return inputStream |
| | | } |
| | | #endif |
| | | |
| | | /// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse` and providing a completion |
| | | /// handler to return a `ResponseDisposition` value. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which the closure will be called. `.main` by default. |
| | | /// - handler: Closure called when the instance produces an `HTTPURLResponse`. The `completionHandler` provided |
| | | /// MUST be called, otherwise the request will never complete. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @_disfavoredOverload |
| | | @discardableResult |
| | | public func onHTTPResponse( |
| | | on queue: DispatchQueue = .main, |
| | | perform handler: @escaping (_ response: HTTPURLResponse, |
| | | _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void |
| | | ) -> Self { |
| | | streamMutableState.write { mutableState in |
| | | mutableState.httpResponseHandler = (queue, handler) |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | /// Sets a closure called whenever the `DataRequest` produces an `HTTPURLResponse`. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which the closure will be called. `.main` by default. |
| | | /// - handler: Closure called when the instance produces an `HTTPURLResponse`. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func onHTTPResponse(on queue: DispatchQueue = .main, |
| | | perform handler: @escaping (HTTPURLResponse) -> Void) -> Self { |
| | | onHTTPResponse(on: queue) { response, completionHandler in |
| | | handler(response) |
| | | completionHandler(.allow) |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | func capturingError(from closure: () throws -> Void) { |
| | | do { |
| | | try closure() |
| | | } catch { |
| | | self.error = error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error))) |
| | | cancel() |
| | | } |
| | | } |
| | | |
| | | func appendStreamCompletion<Success, Failure>(on queue: DispatchQueue, |
| | | stream: @escaping Handler<Success, Failure>) { |
| | | appendResponseSerializer { |
| | | self.underlyingQueue.async { |
| | | self.responseSerializerDidComplete { |
| | | self.streamMutableState.write { state in |
| | | guard state.numberOfExecutingStreams == 0 else { |
| | | state.enqueuedCompletionEvents.append { |
| | | self.enqueueCompletion(on: queue, stream: stream) |
| | | } |
| | | |
| | | return |
| | | } |
| | | |
| | | self.enqueueCompletion(on: queue, stream: stream) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | func enqueueCompletion<Success, Failure>(on queue: DispatchQueue, |
| | | stream: @escaping Handler<Success, Failure>) { |
| | | queue.async { |
| | | do { |
| | | let completion = Completion(request: self.request, |
| | | response: self.response, |
| | | metrics: self.metrics, |
| | | error: self.error) |
| | | try stream(.init(event: .complete(completion), token: .init(self))) |
| | | } catch { |
| | | // Ignore error, as errors on Completion can't be handled anyway. |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension DataStreamRequest.Stream { |
| | | /// Incoming `Result` values from `Event.stream`. |
| | | public var result: Result<Success, Failure>? { |
| | | guard case let .stream(result) = event else { return nil } |
| | | |
| | | return result |
| | | } |
| | | |
| | | /// `Success` value of the instance, if any. |
| | | public var value: Success? { |
| | | guard case let .success(value) = result else { return nil } |
| | | |
| | | return value |
| | | } |
| | | |
| | | /// `Failure` value of the instance, if any. |
| | | public var error: Failure? { |
| | | guard case let .failure(error) = result else { return nil } |
| | | |
| | | return error |
| | | } |
| | | |
| | | /// `Completion` value of the instance, if any. |
| | | public var completion: DataStreamRequest.Completion? { |
| | | guard case let .complete(completion) = event else { return nil } |
| | | |
| | | return completion |
| | | } |
| | | } |
| | | |
| | | // MARK: - DownloadRequest |
| | | |
| | | /// `Request` subclass which downloads `Data` to a file on disk using `URLSessionDownloadTask`. |
| | | public class DownloadRequest: Request { |
| | | /// A set of options to be executed prior to moving a downloaded file from the temporary `URL` to the destination |
| | | /// `URL`. |
| | | public struct Options: OptionSet { |
| | | /// Specifies that intermediate directories for the destination URL should be created. |
| | | public static let createIntermediateDirectories = Options(rawValue: 1 << 0) |
| | | /// Specifies that any previous file at the destination `URL` should be removed. |
| | | public static let removePreviousFile = Options(rawValue: 1 << 1) |
| | | |
| | | public let rawValue: Int |
| | | |
| | | public init(rawValue: Int) { |
| | | self.rawValue = rawValue |
| | | } |
| | | } |
| | | |
| | | // MARK: Destination |
| | | |
| | | /// A closure executed once a `DownloadRequest` has successfully completed in order to determine where to move the |
| | | /// temporary file written to during the download process. The closure takes two arguments: the temporary file URL |
| | | /// and the `HTTPURLResponse`, and returns two values: the file URL where the temporary file should be moved and |
| | | /// the options defining how the file should be moved. |
| | | /// |
| | | /// - Note: Downloads from a local `file://` `URL`s do not use the `Destination` closure, as those downloads do not |
| | | /// return an `HTTPURLResponse`. Instead the file is merely moved within the temporary directory. |
| | | public typealias Destination = (_ temporaryURL: URL, |
| | | _ response: HTTPURLResponse) -> (destinationURL: URL, options: Options) |
| | | |
| | | /// Creates a download file destination closure which uses the default file manager to move the temporary file to a |
| | | /// file URL in the first available directory with the specified search path directory and search path domain mask. |
| | | /// |
| | | /// - Parameters: |
| | | /// - directory: The search path directory. `.documentDirectory` by default. |
| | | /// - domain: The search path domain mask. `.userDomainMask` by default. |
| | | /// - options: `DownloadRequest.Options` used when moving the downloaded file to its destination. None by |
| | | /// default. |
| | | /// - Returns: The `Destination` closure. |
| | | public class func suggestedDownloadDestination(for directory: FileManager.SearchPathDirectory = .documentDirectory, |
| | | in domain: FileManager.SearchPathDomainMask = .userDomainMask, |
| | | options: Options = []) -> Destination { |
| | | { temporaryURL, response in |
| | | let directoryURLs = FileManager.default.urls(for: directory, in: domain) |
| | | let url = directoryURLs.first?.appendingPathComponent(response.suggestedFilename!) ?? temporaryURL |
| | | |
| | | return (url, options) |
| | | } |
| | | } |
| | | |
| | | /// Default `Destination` used by Alamofire to ensure all downloads persist. This `Destination` prepends |
| | | /// `Alamofire_` to the automatically generated download name and moves it within the temporary directory. Files |
| | | /// with this destination must be additionally moved if they should survive the system reclamation of temporary |
| | | /// space. |
| | | static let defaultDestination: Destination = { url, _ in |
| | | (defaultDestinationURL(url), []) |
| | | } |
| | | |
| | | /// Default `URL` creation closure. Creates a `URL` in the temporary directory with `Alamofire_` prepended to the |
| | | /// provided file name. |
| | | static let defaultDestinationURL: (URL) -> URL = { url in |
| | | let filename = "Alamofire_\(url.lastPathComponent)" |
| | | let destination = url.deletingLastPathComponent().appendingPathComponent(filename) |
| | | |
| | | return destination |
| | | } |
| | | |
| | | // MARK: Downloadable |
| | | |
| | | /// Type describing the source used to create the underlying `URLSessionDownloadTask`. |
| | | public enum Downloadable { |
| | | /// Download should be started from the `URLRequest` produced by the associated `URLRequestConvertible` value. |
| | | case request(URLRequestConvertible) |
| | | /// Download should be started from the associated resume `Data` value. |
| | | case resumeData(Data) |
| | | } |
| | | |
| | | // MARK: Mutable State |
| | | |
| | | /// Type containing all mutable state for `DownloadRequest` instances. |
| | | private struct DownloadRequestMutableState { |
| | | /// Possible resume `Data` produced when cancelling the instance. |
| | | var resumeData: Data? |
| | | /// `URL` to which `Data` is being downloaded. |
| | | var fileURL: URL? |
| | | } |
| | | |
| | | /// Protected mutable state specific to `DownloadRequest`. |
| | | private let mutableDownloadState = Protected(DownloadRequestMutableState()) |
| | | |
| | | /// If the download is resumable and is eventually cancelled or fails, this value may be used to resume the download |
| | | /// using the `download(resumingWith data:)` API. |
| | | /// |
| | | /// - Note: For more information about `resumeData`, see [Apple's documentation](https://developer.apple.com/documentation/foundation/urlsessiondownloadtask/1411634-cancel). |
| | | public var resumeData: Data? { |
| | | #if !canImport(FoundationNetworking) // If we not using swift-corelibs-foundation. |
| | | return mutableDownloadState.resumeData ?? error?.downloadResumeData |
| | | #else |
| | | return mutableDownloadState.resumeData |
| | | #endif |
| | | } |
| | | |
| | | /// If the download is successful, the `URL` where the file was downloaded. |
| | | public var fileURL: URL? { mutableDownloadState.fileURL } |
| | | |
| | | // MARK: Initial State |
| | | |
| | | /// `Downloadable` value used for this instance. |
| | | public let downloadable: Downloadable |
| | | /// The `Destination` to which the downloaded file is moved. |
| | | let destination: Destination |
| | | |
| | | /// Creates a `DownloadRequest` using the provided parameters. |
| | | /// |
| | | /// - Parameters: |
| | | /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. |
| | | /// - downloadable: `Downloadable` value used to create `URLSessionDownloadTasks` for the instance. |
| | | /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. |
| | | /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets |
| | | /// `underlyingQueue`, but can be passed another queue from a `Session`. |
| | | /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. |
| | | /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. |
| | | /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request` |
| | | /// - destination: `Destination` closure used to move the downloaded file to its final location. |
| | | init(id: UUID = UUID(), |
| | | downloadable: Downloadable, |
| | | underlyingQueue: DispatchQueue, |
| | | serializationQueue: DispatchQueue, |
| | | eventMonitor: EventMonitor?, |
| | | interceptor: RequestInterceptor?, |
| | | delegate: RequestDelegate, |
| | | destination: @escaping Destination) { |
| | | self.downloadable = downloadable |
| | | self.destination = destination |
| | | |
| | | super.init(id: id, |
| | | underlyingQueue: underlyingQueue, |
| | | serializationQueue: serializationQueue, |
| | | eventMonitor: eventMonitor, |
| | | interceptor: interceptor, |
| | | delegate: delegate) |
| | | } |
| | | |
| | | override func reset() { |
| | | super.reset() |
| | | |
| | | mutableDownloadState.write { |
| | | $0.resumeData = nil |
| | | $0.fileURL = nil |
| | | } |
| | | } |
| | | |
| | | /// Called when a download has finished. |
| | | /// |
| | | /// - Parameters: |
| | | /// - task: `URLSessionTask` that finished the download. |
| | | /// - result: `Result` of the automatic move to `destination`. |
| | | func didFinishDownloading(using task: URLSessionTask, with result: Result<URL, AFError>) { |
| | | eventMonitor?.request(self, didFinishDownloadingUsing: task, with: result) |
| | | |
| | | switch result { |
| | | case let .success(url): mutableDownloadState.fileURL = url |
| | | case let .failure(error): self.error = error |
| | | } |
| | | } |
| | | |
| | | /// Updates the `downloadProgress` using the provided values. |
| | | /// |
| | | /// - Parameters: |
| | | /// - bytesWritten: Total bytes written so far. |
| | | /// - totalBytesExpectedToWrite: Total bytes expected to write. |
| | | func updateDownloadProgress(bytesWritten: Int64, totalBytesExpectedToWrite: Int64) { |
| | | downloadProgress.totalUnitCount = totalBytesExpectedToWrite |
| | | downloadProgress.completedUnitCount += bytesWritten |
| | | |
| | | downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) } |
| | | } |
| | | |
| | | override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { |
| | | session.downloadTask(with: request) |
| | | } |
| | | |
| | | /// Creates a `URLSessionTask` from the provided resume data. |
| | | /// |
| | | /// - Parameters: |
| | | /// - data: `Data` used to resume the download. |
| | | /// - session: `URLSession` used to create the `URLSessionTask`. |
| | | /// |
| | | /// - Returns: The `URLSessionTask` created. |
| | | public func task(forResumeData data: Data, using session: URLSession) -> URLSessionTask { |
| | | session.downloadTask(withResumeData: data) |
| | | } |
| | | |
| | | /// Cancels the instance. Once cancelled, a `DownloadRequest` can no longer be resumed or suspended. |
| | | /// |
| | | /// - Note: This method will NOT produce resume data. If you wish to cancel and produce resume data, use |
| | | /// `cancel(producingResumeData:)` or `cancel(byProducingResumeData:)`. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | override public func cancel() -> Self { |
| | | cancel(producingResumeData: false) |
| | | } |
| | | |
| | | /// Cancels the instance, optionally producing resume data. Once cancelled, a `DownloadRequest` can no longer be |
| | | /// resumed or suspended. |
| | | /// |
| | | /// - Note: If `producingResumeData` is `true`, the `resumeData` property will be populated with any resume data, if |
| | | /// available. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func cancel(producingResumeData shouldProduceResumeData: Bool) -> Self { |
| | | cancel(optionallyProducingResumeData: shouldProduceResumeData ? { _ in } : nil) |
| | | } |
| | | |
| | | /// Cancels the instance while producing resume data. Once cancelled, a `DownloadRequest` can no longer be resumed |
| | | /// or suspended. |
| | | /// |
| | | /// - Note: The resume data passed to the completion handler will also be available on the instance's `resumeData` |
| | | /// property. |
| | | /// |
| | | /// - Parameter completionHandler: The completion handler that is called when the download has been successfully |
| | | /// cancelled. It is not guaranteed to be called on a particular queue, so you may |
| | | /// want use an appropriate queue to perform your work. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func cancel(byProducingResumeData completionHandler: @escaping (_ data: Data?) -> Void) -> Self { |
| | | cancel(optionallyProducingResumeData: completionHandler) |
| | | } |
| | | |
| | | /// Internal implementation of cancellation that optionally takes a resume data handler. If no handler is passed, |
| | | /// cancellation is performed without producing resume data. |
| | | /// |
| | | /// - Parameter completionHandler: Optional resume data handler. |
| | | /// |
| | | /// - Returns: The instance. |
| | | private func cancel(optionallyProducingResumeData completionHandler: ((_ resumeData: Data?) -> Void)?) -> Self { |
| | | mutableState.write { mutableState in |
| | | guard mutableState.state.canTransitionTo(.cancelled) else { return } |
| | | |
| | | mutableState.state = .cancelled |
| | | |
| | | underlyingQueue.async { self.didCancel() } |
| | | |
| | | guard let task = mutableState.tasks.last as? URLSessionDownloadTask, task.state != .completed else { |
| | | underlyingQueue.async { self.finish() } |
| | | return |
| | | } |
| | | |
| | | if let completionHandler = completionHandler { |
| | | // Resume to ensure metrics are gathered. |
| | | task.resume() |
| | | task.cancel { resumeData in |
| | | self.mutableDownloadState.resumeData = resumeData |
| | | self.underlyingQueue.async { self.didCancelTask(task) } |
| | | completionHandler(resumeData) |
| | | } |
| | | } else { |
| | | // Resume to ensure metrics are gathered. |
| | | task.resume() |
| | | task.cancel() |
| | | self.underlyingQueue.async { self.didCancelTask(task) } |
| | | } |
| | | } |
| | | |
| | | return self |
| | | } |
| | | |
| | | /// Validates the request, using the specified closure. |
| | | /// |
| | | /// - Note: If validation fails, subsequent calls to response handlers will have an associated error. |
| | | /// |
| | | /// - Parameter validation: `Validation` closure to validate the response. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func validate(_ validation: @escaping Validation) -> Self { |
| | | let validator: () -> Void = { [unowned self] in |
| | | guard error == nil, let response = response else { return } |
| | | |
| | | let result = validation(request, response, fileURL) |
| | | |
| | | if case let .failure(error) = result { |
| | | self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) |
| | | } |
| | | |
| | | eventMonitor?.request(self, |
| | | didValidateRequest: request, |
| | | response: response, |
| | | fileURL: fileURL, |
| | | withResult: result) |
| | | } |
| | | |
| | | validators.write { $0.append(validator) } |
| | | |
| | | return self |
| | | } |
| | | } |
| | | |
| | | // MARK: - UploadRequest |
| | | |
| | | /// `DataRequest` subclass which handles `Data` upload from memory, file, or stream using `URLSessionUploadTask`. |
| | | public class UploadRequest: DataRequest { |
| | | /// Type describing the origin of the upload, whether `Data`, file, or stream. |
| | | public enum Uploadable { |
| | | /// Upload from the provided `Data` value. |
| | | case data(Data) |
| | | /// Upload from the provided file `URL`, as well as a `Bool` determining whether the source file should be |
| | | /// automatically removed once uploaded. |
| | | case file(URL, shouldRemove: Bool) |
| | | /// Upload from the provided `InputStream`. |
| | | case stream(InputStream) |
| | | } |
| | | |
| | | // MARK: Initial State |
| | | |
| | | /// The `UploadableConvertible` value used to produce the `Uploadable` value for this instance. |
| | | public let upload: UploadableConvertible |
| | | |
| | | /// `FileManager` used to perform cleanup tasks, including the removal of multipart form encoded payloads written |
| | | /// to disk. |
| | | public let fileManager: FileManager |
| | | |
| | | // MARK: Mutable State |
| | | |
| | | /// `Uploadable` value used by the instance. |
| | | public var uploadable: Uploadable? |
| | | |
| | | /// Creates an `UploadRequest` using the provided parameters. |
| | | /// |
| | | /// - Parameters: |
| | | /// - id: `UUID` used for the `Hashable` and `Equatable` implementations. `UUID()` by default. |
| | | /// - convertible: `UploadConvertible` value used to determine the type of upload to be performed. |
| | | /// - underlyingQueue: `DispatchQueue` on which all internal `Request` work is performed. |
| | | /// - serializationQueue: `DispatchQueue` on which all serialization work is performed. By default targets |
| | | /// `underlyingQueue`, but can be passed another queue from a `Session`. |
| | | /// - eventMonitor: `EventMonitor` called for event callbacks from internal `Request` actions. |
| | | /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. |
| | | /// - fileManager: `FileManager` used to perform cleanup tasks, including the removal of multipart form |
| | | /// encoded payloads written to disk. |
| | | /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. |
| | | init(id: UUID = UUID(), |
| | | convertible: UploadConvertible, |
| | | underlyingQueue: DispatchQueue, |
| | | serializationQueue: DispatchQueue, |
| | | eventMonitor: EventMonitor?, |
| | | interceptor: RequestInterceptor?, |
| | | fileManager: FileManager, |
| | | delegate: RequestDelegate) { |
| | | upload = convertible |
| | | self.fileManager = fileManager |
| | | |
| | | super.init(id: id, |
| | | convertible: convertible, |
| | | underlyingQueue: underlyingQueue, |
| | | serializationQueue: serializationQueue, |
| | | eventMonitor: eventMonitor, |
| | | interceptor: interceptor, |
| | | delegate: delegate) |
| | | } |
| | | |
| | | /// Called when the `Uploadable` value has been created from the `UploadConvertible`. |
| | | /// |
| | | /// - Parameter uploadable: The `Uploadable` that was created. |
| | | func didCreateUploadable(_ uploadable: Uploadable) { |
| | | self.uploadable = uploadable |
| | | |
| | | eventMonitor?.request(self, didCreateUploadable: uploadable) |
| | | } |
| | | |
| | | /// Called when the `Uploadable` value could not be created. |
| | | /// |
| | | /// - Parameter error: `AFError` produced by the failure. |
| | | func didFailToCreateUploadable(with error: AFError) { |
| | | self.error = error |
| | | |
| | | eventMonitor?.request(self, didFailToCreateUploadableWithError: error) |
| | | |
| | | retryOrFinish(error: error) |
| | | } |
| | | |
| | | override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask { |
| | | guard let uploadable = uploadable else { |
| | | fatalError("Attempting to create a URLSessionUploadTask when Uploadable value doesn't exist.") |
| | | } |
| | | |
| | | switch uploadable { |
| | | case let .data(data): return session.uploadTask(with: request, from: data) |
| | | case let .file(url, _): return session.uploadTask(with: request, fromFile: url) |
| | | case .stream: return session.uploadTask(withStreamedRequest: request) |
| | | } |
| | | } |
| | | |
| | | override func reset() { |
| | | // Uploadable must be recreated on every retry. |
| | | uploadable = nil |
| | | |
| | | super.reset() |
| | | } |
| | | |
| | | /// Produces the `InputStream` from `uploadable`, if it can. |
| | | /// |
| | | /// - Note: Calling this method with a non-`.stream` `Uploadable` is a logic error and will crash. |
| | | /// |
| | | /// - Returns: The `InputStream`. |
| | | func inputStream() -> InputStream { |
| | | guard let uploadable = uploadable else { |
| | | fatalError("Attempting to access the input stream but the uploadable doesn't exist.") |
| | | } |
| | | |
| | | guard case let .stream(stream) = uploadable else { |
| | | fatalError("Attempted to access the stream of an UploadRequest that wasn't created with one.") |
| | | } |
| | | |
| | | eventMonitor?.request(self, didProvideInputStream: stream) |
| | | |
| | | return stream |
| | | } |
| | | |
| | | override public func cleanup() { |
| | | defer { super.cleanup() } |
| | | |
| | | guard |
| | | let uploadable = uploadable, |
| | | case let .file(url, shouldRemove) = uploadable, |
| | | shouldRemove |
| | | else { return } |
| | | |
| | | try? fileManager.removeItem(at: url) |
| | | } |
| | | } |
| | | |
| | | /// A type that can produce an `UploadRequest.Uploadable` value. |
| | | public protocol UploadableConvertible { |
| | | /// Produces an `UploadRequest.Uploadable` value from the instance. |
| | | /// |
| | | /// - Returns: The `UploadRequest.Uploadable`. |
| | | /// - Throws: Any `Error` produced during creation. |
| | | func createUploadable() throws -> UploadRequest.Uploadable |
| | | } |
| | | |
| | | extension UploadRequest.Uploadable: UploadableConvertible { |
| | | public func createUploadable() throws -> UploadRequest.Uploadable { |
| | | self |
| | | } |
| | | } |
| | | |
| | | /// A type that can be converted to an upload, whether from an `UploadRequest.Uploadable` or `URLRequestConvertible`. |
| | | public protocol UploadConvertible: UploadableConvertible & URLRequestConvertible {} |
Pods/Alamofire/Source/RequestCompression.swift
Pods/Alamofire/Source/RequestInterceptor.swift
Pods/Alamofire/Source/RequestTaskMap.swift
Pods/Alamofire/Source/Response.swift
Pods/Alamofire/Source/ResponseSerialization.swift
Pods/Alamofire/Source/Result+Alamofire.swift
Pods/Alamofire/Source/RetryPolicy.swift
Pods/Alamofire/Source/ServerTrustEvaluation.swift
Pods/Alamofire/Source/Session.swift
Pods/Alamofire/Source/SessionDelegate.swift
Pods/Alamofire/Source/StringEncoding+Alamofire.swift
Pods/Alamofire/Source/URLConvertible+URLRequestConvertible.swift
Pods/Alamofire/Source/URLEncodedFormEncoder.swift
Pods/Alamofire/Source/URLRequest+Alamofire.swift
Pods/Alamofire/Source/URLSessionConfiguration+Alamofire.swift
Pods/Alamofire/Source/Validation.swift
Pods/CryptoSwift/LICENSE
Pods/CryptoSwift/README.md
Pods/CryptoSwift/Sources/CryptoSwift/AEAD/AEAD.swift
Pods/CryptoSwift/Sources/CryptoSwift/AEAD/AEADChaCha20Poly1305.swift
Pods/CryptoSwift/Sources/CryptoSwift/AEAD/AEADXChaCha20Poly1305.swift
Pods/CryptoSwift/Sources/CryptoSwift/AES.Cryptors.swift
Pods/CryptoSwift/Sources/CryptoSwift/AES.swift
Pods/CryptoSwift/Sources/CryptoSwift/ASN1/ASN1.swift
Pods/CryptoSwift/Sources/CryptoSwift/ASN1/ASN1Decoder.swift
Pods/CryptoSwift/Sources/CryptoSwift/ASN1/ASN1Encoder.swift
Pods/CryptoSwift/Sources/CryptoSwift/ASN1/ASN1Scanner.swift
Pods/CryptoSwift/Sources/CryptoSwift/Array+Extension.swift
Pods/CryptoSwift/Sources/CryptoSwift/Authenticator.swift
Pods/CryptoSwift/Sources/CryptoSwift/BatchedCollection.swift
Pods/CryptoSwift/Sources/CryptoSwift/Bit.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockCipher.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockDecryptor.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockEncryptor.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockMode/BlockMode.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockMode/BlockModeOptions.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockMode/CBC.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockMode/CCM.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockMode/CFB.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockMode/CTR.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockMode/CipherModeWorker.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockMode/ECB.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockMode/GCM.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockMode/OCB.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockMode/OFB.swift
Pods/CryptoSwift/Sources/CryptoSwift/BlockMode/PCBC.swift
Pods/CryptoSwift/Sources/CryptoSwift/Blowfish.swift
Pods/CryptoSwift/Sources/CryptoSwift/CBCMAC.swift
Pods/CryptoSwift/Sources/CryptoSwift/CMAC.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Addition.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/BigInt.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/BigUInt.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Bitwise Ops.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/CS.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Codable.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Comparable.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Data Conversion.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Division.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Exponentiation.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Floating Point Conversion.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/GCD.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Hashable.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Integer Conversion.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Multiplication.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Prime Test.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Random.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Shifts.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Square Root.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Strideable.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/String Conversion.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Subtraction.swift
Pods/CryptoSwift/Sources/CryptoSwift/CS_BigInt/Words and Bits.swift
Pods/CryptoSwift/Sources/CryptoSwift/ChaCha20.swift
Pods/CryptoSwift/Sources/CryptoSwift/Checksum.swift
Pods/CryptoSwift/Sources/CryptoSwift/Cipher.swift
Pods/CryptoSwift/Sources/CryptoSwift/Collection+Extension.swift
Pods/CryptoSwift/Sources/CryptoSwift/CompactMap.swift
Pods/CryptoSwift/Sources/CryptoSwift/Cryptor.swift
Pods/CryptoSwift/Sources/CryptoSwift/Cryptors.swift
Pods/CryptoSwift/Sources/CryptoSwift/Digest.swift
Pods/CryptoSwift/Sources/CryptoSwift/DigestType.swift
Pods/CryptoSwift/Sources/CryptoSwift/Foundation/AES+Foundation.swift
Pods/CryptoSwift/Sources/CryptoSwift/Foundation/Array+Foundation.swift
Pods/CryptoSwift/Sources/CryptoSwift/Foundation/Blowfish+Foundation.swift
Pods/CryptoSwift/Sources/CryptoSwift/Foundation/ChaCha20+Foundation.swift
Pods/CryptoSwift/Sources/CryptoSwift/Foundation/Data+Extension.swift
Pods/CryptoSwift/Sources/CryptoSwift/Foundation/HMAC+Foundation.swift
Pods/CryptoSwift/Sources/CryptoSwift/Foundation/Rabbit+Foundation.swift
Pods/CryptoSwift/Sources/CryptoSwift/Foundation/String+FoundationExtension.swift
Pods/CryptoSwift/Sources/CryptoSwift/Foundation/Utils+Foundation.swift
Pods/CryptoSwift/Sources/CryptoSwift/Foundation/XChaCha20+Foundation.swift
Pods/CryptoSwift/Sources/CryptoSwift/Generics.swift
Pods/CryptoSwift/Sources/CryptoSwift/HKDF.swift
Pods/CryptoSwift/Sources/CryptoSwift/HMAC.swift
Pods/CryptoSwift/Sources/CryptoSwift/ISO10126Padding.swift
Pods/CryptoSwift/Sources/CryptoSwift/ISO78164Padding.swift
Pods/CryptoSwift/Sources/CryptoSwift/Int+Extension.swift
Pods/CryptoSwift/Sources/CryptoSwift/MD5.swift
Pods/CryptoSwift/Sources/CryptoSwift/NoPadding.swift
Pods/CryptoSwift/Sources/CryptoSwift/Operators.swift
Pods/CryptoSwift/Sources/CryptoSwift/PEM/DER.swift
Pods/CryptoSwift/Sources/CryptoSwift/PKCS/PBKDF1.swift
Pods/CryptoSwift/Sources/CryptoSwift/PKCS/PBKDF2.swift
Pods/CryptoSwift/Sources/CryptoSwift/PKCS/PKCS1v15.swift
Pods/CryptoSwift/Sources/CryptoSwift/PKCS/PKCS5.swift
Pods/CryptoSwift/Sources/CryptoSwift/PKCS/PKCS7.swift
Pods/CryptoSwift/Sources/CryptoSwift/PKCS/PKCS7Padding.swift
Pods/CryptoSwift/Sources/CryptoSwift/Padding.swift
Pods/CryptoSwift/Sources/CryptoSwift/Poly1305.swift
Pods/CryptoSwift/Sources/CryptoSwift/RSA/RSA+Cipher.swift
Pods/CryptoSwift/Sources/CryptoSwift/RSA/RSA+Signature.swift
Pods/CryptoSwift/Sources/CryptoSwift/RSA/RSA.swift
Pods/CryptoSwift/Sources/CryptoSwift/Rabbit.swift
Pods/CryptoSwift/Sources/CryptoSwift/SHA1.swift
Pods/CryptoSwift/Sources/CryptoSwift/SHA2.swift
Pods/CryptoSwift/Sources/CryptoSwift/SHA3.swift
Pods/CryptoSwift/Sources/CryptoSwift/Scrypt.swift
Pods/CryptoSwift/Sources/CryptoSwift/SecureBytes.swift
Pods/CryptoSwift/Sources/CryptoSwift/Signature.swift
Pods/CryptoSwift/Sources/CryptoSwift/StreamDecryptor.swift
Pods/CryptoSwift/Sources/CryptoSwift/StreamEncryptor.swift
Pods/CryptoSwift/Sources/CryptoSwift/String+Extension.swift
Pods/CryptoSwift/Sources/CryptoSwift/UInt128.swift
Pods/CryptoSwift/Sources/CryptoSwift/UInt16+Extension.swift
Pods/CryptoSwift/Sources/CryptoSwift/UInt32+Extension.swift
Pods/CryptoSwift/Sources/CryptoSwift/UInt64+Extension.swift
Pods/CryptoSwift/Sources/CryptoSwift/UInt8+Extension.swift
Pods/CryptoSwift/Sources/CryptoSwift/Updatable.swift
Pods/CryptoSwift/Sources/CryptoSwift/Utils.swift
Pods/CryptoSwift/Sources/CryptoSwift/XChaCha20.swift
Pods/CryptoSwift/Sources/CryptoSwift/ZeroPadding.swift
Pods/Differentiator/LICENSE.md
Pods/Differentiator/README.md
Pods/Differentiator/Sources/Differentiator/AnimatableSectionModel.swift
Pods/Differentiator/Sources/Differentiator/AnimatableSectionModelType+ItemPath.swift
Pods/Differentiator/Sources/Differentiator/AnimatableSectionModelType.swift
Pods/Differentiator/Sources/Differentiator/Changeset.swift
Pods/Differentiator/Sources/Differentiator/Diff.swift
Pods/Differentiator/Sources/Differentiator/IdentifiableType.swift
Pods/Differentiator/Sources/Differentiator/IdentifiableValue.swift
Pods/Differentiator/Sources/Differentiator/ItemPath.swift
Pods/Differentiator/Sources/Differentiator/Optional+Extensions.swift
Pods/Differentiator/Sources/Differentiator/SectionModel.swift
Pods/Differentiator/Sources/Differentiator/SectionModelType.swift
Pods/Differentiator/Sources/Differentiator/Utilities.swift
Pods/EmptyDataSet-Swift/EmptyDataSet-Swift/Sources/EmptyDataSet.swift
Pods/EmptyDataSet-Swift/EmptyDataSet-Swift/Sources/EmptyDataSetDelegate.swift
Pods/EmptyDataSet-Swift/EmptyDataSet-Swift/Sources/EmptyDataSetSource.swift
Pods/EmptyDataSet-Swift/EmptyDataSet-Swift/Sources/EmptyDataSetView+Extension.swift
Pods/EmptyDataSet-Swift/EmptyDataSet-Swift/Sources/EmptyDataSetView.swift
Pods/EmptyDataSet-Swift/LICENSE
Pods/EmptyDataSet-Swift/README.md
Pods/FFPage/FFPage/Controller/FFAdapterViewController.h
Pods/FFPage/FFPage/Controller/FFAdapterViewController.m
Pods/FFPage/FFPage/Controller/FFPageViewController.h
Pods/FFPage/FFPage/Controller/FFPageViewController.m
Pods/FFPage/FFPage/FFPage.h
Pods/FFPage/FFPage/Protocol/FFPageProtocol.h
Pods/FFPage/FFPage/Refresh/FFRereshView.h
Pods/FFPage/FFPage/Refresh/FFRereshView.m
Pods/FFPage/FFPage/Utils/FFDynamicItem.h
Pods/FFPage/FFPage/Utils/FFDynamicItem.m
Pods/FFPage/FFPage/Utils/UIScrollView+FFPage.h
Pods/FFPage/FFPage/Utils/UIScrollView+FFPage.m
Pods/FFPage/LICENSE
Pods/FFPage/README.md
Pods/HandyJSON/LICENSE
Pods/HandyJSON/README.md
Pods/HandyJSON/Source/AnyExtensions.swift
Pods/HandyJSON/Source/BuiltInBasicType.swift
Pods/HandyJSON/Source/BuiltInBridgeType.swift
Pods/HandyJSON/Source/CBridge.swift
Pods/HandyJSON/Source/Configuration.swift
Pods/HandyJSON/Source/ContextDescriptorType.swift
Pods/HandyJSON/Source/CustomDateFormatTransform.swift
Pods/HandyJSON/Source/DataTransform.swift
Pods/HandyJSON/Source/DateFormatterTransform.swift
Pods/HandyJSON/Source/DateTransform.swift
Pods/HandyJSON/Source/Deserializer.swift
Pods/HandyJSON/Source/EnumTransform.swift
Pods/HandyJSON/Source/EnumType.swift
Pods/HandyJSON/Source/Export.swift
Pods/HandyJSON/Source/ExtendCustomBasicType.swift
Pods/HandyJSON/Source/ExtendCustomModelType.swift
Pods/HandyJSON/Source/FieldDescriptor.swift
Pods/HandyJSON/Source/HandyJSON.h
Pods/HandyJSON/Source/HelpingMapper.swift
Pods/HandyJSON/Source/HexColorTransform.swift
Pods/HandyJSON/Source/ISO8601DateTransform.swift
Pods/HandyJSON/Source/Logger.swift
Pods/HandyJSON/Source/MangledName.swift
Pods/HandyJSON/Source/Measuable.swift
Pods/HandyJSON/Source/Metadata.swift
Pods/HandyJSON/Source/NSDecimalNumberTransform.swift
Pods/HandyJSON/Source/OtherExtension.swift
Pods/HandyJSON/Source/PointerType.swift
Pods/HandyJSON/Source/Properties.swift
Pods/HandyJSON/Source/PropertyInfo.swift
Pods/HandyJSON/Source/ReflectionHelper.swift
Pods/HandyJSON/Source/Serializer.swift
Pods/HandyJSON/Source/TransformOf.swift
Pods/HandyJSON/Source/TransformType.swift
Pods/HandyJSON/Source/Transformable.swift
Pods/HandyJSON/Source/URLTransform.swift
Pods/IQKeyboardManager/IQKeyboardManager/Categories/IQNSArray+Sort.h
Pods/IQKeyboardManager/IQKeyboardManager/Categories/IQNSArray+Sort.m
Pods/IQKeyboardManager/IQKeyboardManager/Categories/IQUIScrollView+Additions.h
Pods/IQKeyboardManager/IQKeyboardManager/Categories/IQUIScrollView+Additions.m
Pods/IQKeyboardManager/IQKeyboardManager/Categories/IQUITextFieldView+Additions.h
Pods/IQKeyboardManager/IQKeyboardManager/Categories/IQUITextFieldView+Additions.m
Pods/IQKeyboardManager/IQKeyboardManager/Categories/IQUIView+Hierarchy.h
Pods/IQKeyboardManager/IQKeyboardManager/Categories/IQUIView+Hierarchy.m
Pods/IQKeyboardManager/IQKeyboardManager/Categories/IQUIViewController+Additions.h
Pods/IQKeyboardManager/IQKeyboardManager/Categories/IQUIViewController+Additions.m
Pods/IQKeyboardManager/IQKeyboardManager/Constants/IQKeyboardManagerConstants.h
Pods/IQKeyboardManager/IQKeyboardManager/Constants/IQKeyboardManagerConstantsInternal.h
Pods/IQKeyboardManager/IQKeyboardManager/IQKeyboardManager.h
Pods/IQKeyboardManager/IQKeyboardManager/IQKeyboardManager.m
Pods/IQKeyboardManager/IQKeyboardManager/IQKeyboardReturnKeyHandler.h
Pods/IQKeyboardManager/IQKeyboardManager/IQKeyboardReturnKeyHandler.m
Pods/IQKeyboardManager/IQKeyboardManager/IQTextView/IQTextView.h
Pods/IQKeyboardManager/IQKeyboardManager/IQTextView/IQTextView.m
Pods/IQKeyboardManager/IQKeyboardManager/IQToolbar/IQBarButtonItem.h
Pods/IQKeyboardManager/IQKeyboardManager/IQToolbar/IQBarButtonItem.m
Pods/IQKeyboardManager/IQKeyboardManager/IQToolbar/IQPreviousNextView.h
Pods/IQKeyboardManager/IQKeyboardManager/IQToolbar/IQPreviousNextView.m
Pods/IQKeyboardManager/IQKeyboardManager/IQToolbar/IQTitleBarButtonItem.h
Pods/IQKeyboardManager/IQKeyboardManager/IQToolbar/IQTitleBarButtonItem.m
Pods/IQKeyboardManager/IQKeyboardManager/IQToolbar/IQToolbar.h
Pods/IQKeyboardManager/IQKeyboardManager/IQToolbar/IQToolbar.m
Pods/IQKeyboardManager/IQKeyboardManager/IQToolbar/IQUIView+IQKeyboardToolbar.h
Pods/IQKeyboardManager/IQKeyboardManager/IQToolbar/IQUIView+IQKeyboardToolbar.m
Pods/IQKeyboardManager/IQKeyboardManager/PrivacyInfo.xcprivacy
Pods/IQKeyboardManager/LICENSE.md
Pods/IQKeyboardManager/README.md
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/Categories/IQNSArray+Sort.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/Categories/IQUIScrollView+Additions.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/Categories/IQUITextFieldView+Additions.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/Categories/IQUIView+Hierarchy.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/Categories/IQUIViewController+Additions.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/Constants/IQKeyboardManagerConstants.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/Constants/IQKeyboardManagerConstantsInternal.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQKeyboardManager+Debug.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQKeyboardManager+Internal.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQKeyboardManager+OrientationNotification.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQKeyboardManager+Position.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQKeyboardManager+Toolbar.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQKeyboardManager+UIKeyboardNotification.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQKeyboardManager+UITextFieldViewNotification.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQKeyboardManager.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQKeyboardReturnKeyHandler.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQTextView/IQPlaceholderable.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQTextView/IQTextView.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQToolbar/IQBarButtonItem.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQToolbar/IQInvocation.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQToolbar/IQPreviousNextView.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQToolbar/IQTitleBarButtonItem.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQToolbar/IQToolbar.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/IQToolbar/IQUIView+IQKeyboardToolbar.swift
Pods/IQKeyboardManagerSwift/IQKeyboardManagerSwift/PrivacyInfo.xcprivacy
Pods/IQKeyboardManagerSwift/LICENSE.md
Pods/IQKeyboardManagerSwift/README.md
Pods/Lantern/LICENSE
Pods/Lantern/README.md
Pods/Lantern/Sources/Lantern/Lantern.swift
Pods/Lantern/Sources/Lantern/LanternAnimatedTransitioning.swift
Pods/Lantern/Sources/Lantern/LanternCell.swift
Pods/Lantern/Sources/Lantern/LanternDefaultPageIndicator.swift
Pods/Lantern/Sources/Lantern/LanternFadeAnimator.swift
Pods/Lantern/Sources/Lantern/LanternImageCell.swift
Pods/Lantern/Sources/Lantern/LanternLog.swift
Pods/Lantern/Sources/Lantern/LanternNoneAnimator.swift
Pods/Lantern/Sources/Lantern/LanternNumberPageIndicator.swift
Pods/Lantern/Sources/Lantern/LanternPageIndicator.swift
Pods/Lantern/Sources/Lantern/LanternPhotoVideoCell.swift
Pods/Lantern/Sources/Lantern/LanternSmoothZoomAnimator.swift
Pods/Lantern/Sources/Lantern/LanternVideoPlayer.swift
Pods/Lantern/Sources/Lantern/LanternView.swift
Pods/Lantern/Sources/Lantern/LanternZoomAnimator.swift
Pods/Lantern/Sources/Lantern/LanternZoomSupportedCell.swift
Pods/Local Podspecs/JQTools.podspec.json
Pods/MJRefresh/LICENSE
Pods/MJRefresh/MJRefresh/Base/MJRefreshAutoFooter.h
Pods/MJRefresh/MJRefresh/Base/MJRefreshAutoFooter.m
Pods/MJRefresh/MJRefresh/Base/MJRefreshBackFooter.h
Pods/MJRefresh/MJRefresh/Base/MJRefreshBackFooter.m
Pods/MJRefresh/MJRefresh/Base/MJRefreshComponent.h
Pods/MJRefresh/MJRefresh/Base/MJRefreshComponent.m
Pods/MJRefresh/MJRefresh/Base/MJRefreshFooter.h
Pods/MJRefresh/MJRefresh/Base/MJRefreshFooter.m
Pods/MJRefresh/MJRefresh/Base/MJRefreshHeader.h
Pods/MJRefresh/MJRefresh/Base/MJRefreshHeader.m
Pods/MJRefresh/MJRefresh/Base/MJRefreshTrailer.h
Pods/MJRefresh/MJRefresh/Base/MJRefreshTrailer.m
Pods/MJRefresh/MJRefresh/Custom/Footer/Auto/MJRefreshAutoGifFooter.h
Pods/MJRefresh/MJRefresh/Custom/Footer/Auto/MJRefreshAutoGifFooter.m
Pods/MJRefresh/MJRefresh/Custom/Footer/Auto/MJRefreshAutoNormalFooter.h
Pods/MJRefresh/MJRefresh/Custom/Footer/Auto/MJRefreshAutoNormalFooter.m
Pods/MJRefresh/MJRefresh/Custom/Footer/Auto/MJRefreshAutoStateFooter.h
Pods/MJRefresh/MJRefresh/Custom/Footer/Auto/MJRefreshAutoStateFooter.m
Pods/MJRefresh/MJRefresh/Custom/Footer/Back/MJRefreshBackGifFooter.h
Pods/MJRefresh/MJRefresh/Custom/Footer/Back/MJRefreshBackGifFooter.m
Pods/MJRefresh/MJRefresh/Custom/Footer/Back/MJRefreshBackNormalFooter.h
Pods/MJRefresh/MJRefresh/Custom/Footer/Back/MJRefreshBackNormalFooter.m
Pods/MJRefresh/MJRefresh/Custom/Footer/Back/MJRefreshBackStateFooter.h
Pods/MJRefresh/MJRefresh/Custom/Footer/Back/MJRefreshBackStateFooter.m
Pods/MJRefresh/MJRefresh/Custom/Header/MJRefreshGifHeader.h
Pods/MJRefresh/MJRefresh/Custom/Header/MJRefreshGifHeader.m
Pods/MJRefresh/MJRefresh/Custom/Header/MJRefreshNormalHeader.h
Pods/MJRefresh/MJRefresh/Custom/Header/MJRefreshNormalHeader.m
Pods/MJRefresh/MJRefresh/Custom/Header/MJRefreshStateHeader.h
Pods/MJRefresh/MJRefresh/Custom/Header/MJRefreshStateHeader.m
Pods/MJRefresh/MJRefresh/Custom/Trailer/MJRefreshNormalTrailer.h
Pods/MJRefresh/MJRefresh/Custom/Trailer/MJRefreshNormalTrailer.m
Pods/MJRefresh/MJRefresh/Custom/Trailer/MJRefreshStateTrailer.h
Pods/MJRefresh/MJRefresh/Custom/Trailer/MJRefreshStateTrailer.m
Pods/MJRefresh/MJRefresh/MJRefresh.bundle/arrow@2x.png
Pods/MJRefresh/MJRefresh/MJRefresh.bundle/en.lproj/Localizable.strings
Pods/MJRefresh/MJRefresh/MJRefresh.bundle/ko.lproj/Localizable.strings
Pods/MJRefresh/MJRefresh/MJRefresh.bundle/ru.lproj/Localizable.strings
Pods/MJRefresh/MJRefresh/MJRefresh.bundle/trail_arrow@2x.png
Pods/MJRefresh/MJRefresh/MJRefresh.bundle/uk.lproj/Localizable.strings
Pods/MJRefresh/MJRefresh/MJRefresh.bundle/zh-Hans.lproj/Localizable.strings
Pods/MJRefresh/MJRefresh/MJRefresh.bundle/zh-Hant.lproj/Localizable.strings
Pods/MJRefresh/MJRefresh/MJRefresh.h
Pods/MJRefresh/MJRefresh/MJRefreshConfig.h
Pods/MJRefresh/MJRefresh/MJRefreshConfig.m
Pods/MJRefresh/MJRefresh/MJRefreshConst.h
Pods/MJRefresh/MJRefresh/MJRefreshConst.m
Pods/MJRefresh/MJRefresh/NSBundle+MJRefresh.h
Pods/MJRefresh/MJRefresh/NSBundle+MJRefresh.m
Pods/MJRefresh/MJRefresh/UICollectionViewLayout+MJRefresh.h
Pods/MJRefresh/MJRefresh/UICollectionViewLayout+MJRefresh.m
Pods/MJRefresh/MJRefresh/UIScrollView+MJExtension.h
Pods/MJRefresh/MJRefresh/UIScrollView+MJExtension.m
Pods/MJRefresh/MJRefresh/UIScrollView+MJRefresh.h
Pods/MJRefresh/MJRefresh/UIScrollView+MJRefresh.m
Pods/MJRefresh/MJRefresh/UIView+MJExtension.h
Pods/MJRefresh/MJRefresh/UIView+MJExtension.m
Pods/MJRefresh/README.md
Pods/Manifest.lock
Pods/ObjcExceptionBridging/LICENSE.txt
Pods/ObjcExceptionBridging/README.md
Pods/ObjcExceptionBridging/Sources/ObjcExceptionBridging/ObjectiveCMarker.m
Pods/ObjcExceptionBridging/Sources/ObjcExceptionBridging/include/ObjcExceptionBridging.h
Pods/ObjectMapper/LICENSE
Pods/ObjectMapper/README-CN.md
Pods/ObjectMapper/Sources/CodableTransform.swift
Pods/ObjectMapper/Sources/CustomDateFormatTransform.swift
Pods/ObjectMapper/Sources/DataTransform.swift
Pods/ObjectMapper/Sources/DateFormatterTransform.swift
Pods/ObjectMapper/Sources/DateTransform.swift
Pods/ObjectMapper/Sources/DictionaryTransform.swift
Pods/ObjectMapper/Sources/EnumOperators.swift
Pods/ObjectMapper/Sources/EnumTransform.swift
Pods/ObjectMapper/Sources/FromJSON.swift
Pods/ObjectMapper/Sources/HexColorTransform.swift
Pods/ObjectMapper/Sources/ISO8601DateTransform.swift
Pods/ObjectMapper/Sources/ImmutableMappable.swift
Pods/ObjectMapper/Sources/IntegerOperators.swift
Pods/ObjectMapper/Sources/Map.swift
Pods/ObjectMapper/Sources/MapError.swift
Pods/ObjectMapper/Sources/Mappable.swift
Pods/ObjectMapper/Sources/Mapper.swift
Pods/ObjectMapper/Sources/NSDecimalNumberTransform.swift
Pods/ObjectMapper/Sources/Operators.swift
Pods/ObjectMapper/Sources/ToJSON.swift
Pods/ObjectMapper/Sources/TransformOf.swift
Pods/ObjectMapper/Sources/TransformOperators.swift
Pods/ObjectMapper/Sources/TransformType.swift
Pods/ObjectMapper/Sources/URLTransform.swift
Pods/Pods.xcodeproj/project.pbxproj
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/Alamofire.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/CryptoSwift.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/Differentiator.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/EmptyDataSet-Swift.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/FFPage.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/HandyJSON.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/IQKeyboardManager.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/IQKeyboardManagerSwift.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/JQTools-JQToolsRes.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/JQTools.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/Lantern.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/MJRefresh.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/ObjcExceptionBridging.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/ObjectMapper.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/Pods-DolphinEnglishLearnStudent.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/QMUIKit-QMUIResources.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/QMUIKit.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/RxCocoa.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/RxDataSources.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/RxRelay.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/RxSwift.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/SDWebImage.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/SPPageMenu.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/SVProgressHUD.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/SnapKit.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/SwifterSwift.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/SwiftyStoreKit.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/TZImagePickerController.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/UserDefaultsStore.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/VTMagic.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/WechatOpenSDK-XCFramework.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/XCGLogger.xcscheme
Pods/Pods.xcodeproj/xcuserdata/yvkd.xcuserdatad/xcschemes/xcschememanagement.plist
Pods/QMUIKit/LICENSE.TXT
Pods/QMUIKit/QMUIConfigurationTemplate/QMUIConfigurationTemplate.h
Pods/QMUIKit/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m
Pods/QMUIKit/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.h
Pods/QMUIKit/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m
Pods/QMUIKit/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.h
Pods/QMUIKit/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.m
Pods/QMUIKit/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.h
Pods/QMUIKit/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.m
Pods/QMUIKit/QMUIKit/QMUIComponents/CAAnimation+QMUI.h
Pods/QMUIKit/QMUIKit/QMUIComponents/CAAnimation+QMUI.m
Pods/QMUIKit/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.h
Pods/QMUIKit/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.m
Pods/QMUIKit/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.h
Pods/QMUIKit/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.m
Pods/QMUIKit/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h
Pods/QMUIKit/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m
Pods/QMUIKit/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h
Pods/QMUIKit/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m
Pods/QMUIKit/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h
Pods/QMUIKit/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m
Pods/QMUIKit/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h
Pods/QMUIKit/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m
Pods/QMUIKit/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h
Pods/QMUIKit/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m
Pods/QMUIKit/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.h
Pods/QMUIKit/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIAlertController.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIAlertController.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIAnimation/QMUIEasings.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIAppearance.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIAppearance.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeProtocol.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICellHeightCache.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICellHeightCache.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIDialogViewController.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIDialogViewController.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIEmotionInputManager.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIEmotionInputManager.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIEmotionView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIEmotionView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIEmptyView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIEmptyView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIFloatLayoutView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIFloatLayoutView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIGridView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIGridView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIKeyboardManager.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIKeyboardManager.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILabel.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILabel.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILog/QMUILog.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILog/QMUILogger.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILog/QMUILogger.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILogManagerViewController.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILogManagerViewController.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIMarqueeLabel.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIMoreOperationController.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIMoreOperationController.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUINavigationTitleView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUINavigationTitleView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIOrderedDictionary.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIOrderedDictionary.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPieProgressView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPieProgressView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupContainerView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupContainerView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuBaseItem.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuBaseItem.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemProtocol.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUISearchBar.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUISearchBar.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUISearchController.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUISearchController.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUISegmentedControl.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUISegmentedControl.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITableView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITableView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITableViewCell.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITableViewCell.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITableViewProtocols.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITestView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITestView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITextField.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITextField.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITextView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITextView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/QMUITheme.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManagerCenter.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManagerCenter.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIViewController+QMUITheme.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIViewController+QMUITheme.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIVisualEffect+QMUITheme.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIVisualEffect+QMUITheme.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITips.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITips.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIZoomImageView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIZoomImageView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.h
Pods/QMUIKit/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.m
Pods/QMUIKit/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.h
Pods/QMUIKit/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m
Pods/QMUIKit/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.h
Pods/QMUIKit/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.m
Pods/QMUIKit/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.h
Pods/QMUIKit/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.m
Pods/QMUIKit/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.m
Pods/QMUIKit/QMUIKit/QMUIComponents/ToastView/QMUIToastView.h
Pods/QMUIKit/QMUIKit/QMUIComponents/ToastView/QMUIToastView.m
Pods/QMUIKit/QMUIKit/QMUICore/QMUICommonDefines.h
Pods/QMUIKit/QMUIKit/QMUICore/QMUIConfiguration.h
Pods/QMUIKit/QMUIKit/QMUICore/QMUIConfiguration.m
Pods/QMUIKit/QMUIKit/QMUICore/QMUIConfigurationMacros.h
Pods/QMUIKit/QMUIKit/QMUICore/QMUICore.h
Pods/QMUIKit/QMUIKit/QMUICore/QMUIHelper.h
Pods/QMUIKit/QMUIKit/QMUICore/QMUIHelper.m
Pods/QMUIKit/QMUIKit/QMUICore/QMUILab.h
Pods/QMUIKit/QMUIKit/QMUICore/QMUIRuntime.h
Pods/QMUIKit/QMUIKit/QMUICore/QMUIRuntime.m
Pods/QMUIKit/QMUIKit/QMUIKit.h
Pods/QMUIKit/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.h
Pods/QMUIKit/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m
Pods/QMUIKit/QMUIKit/QMUIMainFrame/QMUICommonViewController.h
Pods/QMUIKit/QMUIKit/QMUIMainFrame/QMUICommonViewController.m
Pods/QMUIKit/QMUIKit/QMUIMainFrame/QMUINavigationController.h
Pods/QMUIKit/QMUIKit/QMUIMainFrame/QMUINavigationController.m
Pods/QMUIKit/QMUIKit/QMUIMainFrame/QMUITabBarViewController.h
Pods/QMUIKit/QMUIKit/QMUIMainFrame/QMUITabBarViewController.m
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_clear.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_clear.imageset/QMUI_console_clear.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter.imageset/QMUI_console_filter.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter_selected.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter_selected.imageset/QMUI_console_filter_selected.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_logo.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_logo.imageset/QMUI_console_logo.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_emotion_delete.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_emotion_delete.imageset/QMUI_emotion_delete.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/QMUI_hiddenAlbum.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_icloud_download_fault.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_icloud_download_fault.imageset/QMUI_icloud_download_fault.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox.imageset/QMUI_pickerImage_checkbox.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox_checked.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox_checked.imageset/QMUI_pickerImage_checkbox_checked.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_favorite.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_favorite.imageset/QMUI_pickerImage_favorite.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_video_mark.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_video_mark.imageset/QMUI_pickerImage_video_mark.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox.imageset/QMUI_previewImage_checkbox.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox_checked.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox_checked.imageset/QMUI_previewImage_checkbox_checked.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_done.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_done.imageset/QMUI_tips_done.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_error.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_error.imageset/QMUI_tips_error.pdf
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_info.imageset/Contents.json
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_info.imageset/QMUI_tips_info.pdf
Pods/QMUIKit/QMUIKit/UIKitExtensions/CALayer+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/CALayer+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSArray+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSArray+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSDictionary+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSDictionary+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSNumber+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSNumber+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSObject+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSObject+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSShadow+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSShadow+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSString+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSString+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSURL+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSURL+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocol.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIStringPrivate.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIStringPrivate.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIApplication+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIApplication+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIBarItem+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIBlurEffect+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIBlurEffect+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIButton+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIButton+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UICollectionView+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UICollectionView+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UICollectionViewCell+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UICollectionViewCell+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIColor+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIColor+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIControl+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIControl+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIFont+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIFont+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIImage+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIImage+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIImageView+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIImageView+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIInterface+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIInterface+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UILabel+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UILabel+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIMenuController+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIMenuController+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UINavigationItem+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UINavigationItem+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIScrollView+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIScrollView+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISearchBar+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISearchController+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISearchController+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISlider+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISlider+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISwitch+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISwitch+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITabBar+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITabBar+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITableView+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITableView+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITableViewHeaderFooterView+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITableViewHeaderFooterView+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITextField+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITextField+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITextInputTraits+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITextInputTraits+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITextView+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITextView+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIToolbar+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIToolbar+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITraitCollection+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITraitCollection+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIView+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIView+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIView+QMUIBorder.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIView+QMUIBorder.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIViewController+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIViewController+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIVisualEffectView+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIVisualEffectView+QMUI.m
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIWindow+QMUI.h
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIWindow+QMUI.m
Pods/QMUIKit/README.md
Pods/RxCocoa/LICENSE.md
Pods/RxCocoa/Platform/DataStructures/Bag.swift
Pods/RxCocoa/Platform/DataStructures/InfiniteSequence.swift
Pods/RxCocoa/Platform/DataStructures/PriorityQueue.swift
Pods/RxCocoa/Platform/DataStructures/Queue.swift
Pods/RxCocoa/Platform/DispatchQueue+Extensions.swift
Pods/RxCocoa/Platform/Platform.Darwin.swift
Pods/RxCocoa/Platform/Platform.Linux.swift
Pods/RxCocoa/Platform/RecursiveLock.swift
Pods/RxCocoa/README.md
Pods/RxCocoa/RxCocoa/Common/ControlTarget.swift
Pods/RxCocoa/RxCocoa/Common/DelegateProxy.swift
Pods/RxCocoa/RxCocoa/Common/DelegateProxyType.swift
Pods/RxCocoa/RxCocoa/Common/Infallible+Bind.swift
Pods/RxCocoa/RxCocoa/Common/Observable+Bind.swift
Pods/RxCocoa/RxCocoa/Common/RxCocoaObjCRuntimeError+Extensions.swift
Pods/RxCocoa/RxCocoa/Common/RxTarget.swift
Pods/RxCocoa/RxCocoa/Common/SectionedViewDataSourceType.swift
Pods/RxCocoa/RxCocoa/Common/TextInput.swift
Pods/RxCocoa/RxCocoa/Foundation/KVORepresentable+CoreGraphics.swift
Pods/RxCocoa/RxCocoa/Foundation/KVORepresentable+Swift.swift
Pods/RxCocoa/RxCocoa/Foundation/KVORepresentable.swift
Pods/RxCocoa/RxCocoa/Foundation/NSObject+Rx+KVORepresentable.swift
Pods/RxCocoa/RxCocoa/Foundation/NSObject+Rx+RawRepresentable.swift
Pods/RxCocoa/RxCocoa/Foundation/NSObject+Rx.swift
Pods/RxCocoa/RxCocoa/Foundation/NotificationCenter+Rx.swift
Pods/RxCocoa/RxCocoa/Foundation/URLSession+Rx.swift
Pods/RxCocoa/RxCocoa/Runtime/_RX.m
Pods/RxCocoa/RxCocoa/Runtime/_RXDelegateProxy.m
Pods/RxCocoa/RxCocoa/Runtime/_RXKVOObserver.m
Pods/RxCocoa/RxCocoa/Runtime/_RXObjCRuntime.m
Pods/RxCocoa/RxCocoa/Runtime/include/RxCocoaRuntime.h
Pods/RxCocoa/RxCocoa/Runtime/include/_RX.h
Pods/RxCocoa/RxCocoa/Runtime/include/_RXDelegateProxy.h
Pods/RxCocoa/RxCocoa/Runtime/include/_RXKVOObserver.h
Pods/RxCocoa/RxCocoa/Runtime/include/_RXObjCRuntime.h
Pods/RxCocoa/RxCocoa/RxCocoa.h
Pods/RxCocoa/RxCocoa/RxCocoa.swift
Pods/RxCocoa/RxCocoa/Traits/ControlEvent.swift
Pods/RxCocoa/RxCocoa/Traits/ControlProperty.swift
Pods/RxCocoa/RxCocoa/Traits/Driver/BehaviorRelay+Driver.swift
Pods/RxCocoa/RxCocoa/Traits/Driver/ControlEvent+Driver.swift
Pods/RxCocoa/RxCocoa/Traits/Driver/ControlProperty+Driver.swift
Pods/RxCocoa/RxCocoa/Traits/Driver/Driver+Subscription.swift
Pods/RxCocoa/RxCocoa/Traits/Driver/Driver.swift
Pods/RxCocoa/RxCocoa/Traits/Driver/Infallible+Driver.swift
Pods/RxCocoa/RxCocoa/Traits/Driver/ObservableConvertibleType+Driver.swift
Pods/RxCocoa/RxCocoa/Traits/SharedSequence/ObservableConvertibleType+SharedSequence.swift
Pods/RxCocoa/RxCocoa/Traits/SharedSequence/SchedulerType+SharedSequence.swift
Pods/RxCocoa/RxCocoa/Traits/SharedSequence/SharedSequence+Concurrency.swift
Pods/RxCocoa/RxCocoa/Traits/SharedSequence/SharedSequence+Operators+arity.swift
Pods/RxCocoa/RxCocoa/Traits/SharedSequence/SharedSequence+Operators.swift
Pods/RxCocoa/RxCocoa/Traits/SharedSequence/SharedSequence.swift
Pods/RxCocoa/RxCocoa/Traits/Signal/ControlEvent+Signal.swift
Pods/RxCocoa/RxCocoa/Traits/Signal/ObservableConvertibleType+Signal.swift
Pods/RxCocoa/RxCocoa/Traits/Signal/PublishRelay+Signal.swift
Pods/RxCocoa/RxCocoa/Traits/Signal/Signal+Subscription.swift
Pods/RxCocoa/RxCocoa/Traits/Signal/Signal.swift
Pods/RxCocoa/RxCocoa/iOS/DataSources/RxCollectionViewReactiveArrayDataSource.swift
Pods/RxCocoa/RxCocoa/iOS/DataSources/RxPickerViewAdapter.swift
Pods/RxCocoa/RxCocoa/iOS/DataSources/RxTableViewReactiveArrayDataSource.swift
Pods/RxCocoa/RxCocoa/iOS/Events/ItemEvents.swift
Pods/RxCocoa/RxCocoa/iOS/NSTextStorage+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/Protocols/RxCollectionViewDataSourceType.swift
Pods/RxCocoa/RxCocoa/iOS/Protocols/RxPickerViewDataSourceType.swift
Pods/RxCocoa/RxCocoa/iOS/Protocols/RxTableViewDataSourceType.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxCollectionViewDataSourcePrefetchingProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxCollectionViewDataSourceProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxCollectionViewDelegateProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxDelegateProxyCrashFix.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxNavigationControllerDelegateProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxPickerViewDataSourceProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxPickerViewDelegateProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxScrollViewDelegateProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxSearchBarDelegateProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxSearchControllerDelegateProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxTabBarControllerDelegateProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxTabBarDelegateProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxTableViewDataSourcePrefetchingProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxTableViewDataSourceProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxTableViewDelegateProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxTextStorageDelegateProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxTextViewDelegateProxy.swift
Pods/RxCocoa/RxCocoa/iOS/Proxies/RxWKNavigationDelegateProxy.swift
Pods/RxCocoa/RxCocoa/iOS/UIActivityIndicatorView+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UIApplication+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UIBarButtonItem+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UIButton+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UICollectionView+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UIControl+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UIDatePicker+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UIGestureRecognizer+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UINavigationController+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UIPickerView+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UIRefreshControl+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UIScrollView+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UISearchBar+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UISearchController+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UISegmentedControl+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UISlider+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UIStepper+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UISwitch+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UITabBar+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UITabBarController+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UITableView+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UITextField+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/UITextView+Rx.swift
Pods/RxCocoa/RxCocoa/iOS/WKWebView+Rx.swift
Pods/RxCocoa/RxCocoa/macOS/NSButton+Rx.swift
Pods/RxCocoa/RxCocoa/macOS/NSControl+Rx.swift
Pods/RxCocoa/RxCocoa/macOS/NSSlider+Rx.swift
Pods/RxCocoa/RxCocoa/macOS/NSTextField+Rx.swift
Pods/RxCocoa/RxCocoa/macOS/NSTextView+Rx.swift
Pods/RxCocoa/RxCocoa/macOS/NSView+Rx.swift
Pods/RxDataSources/LICENSE.md
Pods/RxDataSources/README.md
Pods/RxDataSources/Sources/RxDataSources/AnimationConfiguration.swift
Pods/RxDataSources/Sources/RxDataSources/Array+Extensions.swift
Pods/RxDataSources/Sources/RxDataSources/CollectionViewSectionedDataSource.swift
Pods/RxDataSources/Sources/RxDataSources/DataSources.swift
Pods/RxDataSources/Sources/RxDataSources/Deprecated.swift
Pods/RxDataSources/Sources/RxDataSources/FloatingPointType+IdentifiableType.swift
Pods/RxDataSources/Sources/RxDataSources/IntegerType+IdentifiableType.swift
Pods/RxDataSources/Sources/RxDataSources/RxCollectionViewSectionedAnimatedDataSource.swift
Pods/RxDataSources/Sources/RxDataSources/RxCollectionViewSectionedReloadDataSource.swift
Pods/RxDataSources/Sources/RxDataSources/RxPickerViewAdapter.swift
Pods/RxDataSources/Sources/RxDataSources/RxTableViewSectionedAnimatedDataSource.swift
Pods/RxDataSources/Sources/RxDataSources/RxTableViewSectionedReloadDataSource.swift
Pods/RxDataSources/Sources/RxDataSources/String+IdentifiableType.swift
Pods/RxDataSources/Sources/RxDataSources/TableViewSectionedDataSource.swift
Pods/RxDataSources/Sources/RxDataSources/UI+SectionedViewType.swift
Pods/RxDataSources/Sources/RxDataSources/ViewTransition.swift
Pods/RxRelay/LICENSE.md
Pods/RxRelay/README.md
Pods/RxRelay/RxRelay/BehaviorRelay.swift
Pods/RxRelay/RxRelay/Observable+Bind.swift
Pods/RxRelay/RxRelay/PublishRelay.swift
Pods/RxRelay/RxRelay/ReplayRelay.swift
Pods/RxRelay/RxRelay/Utils.swift
Pods/RxSwift/LICENSE.md
Pods/RxSwift/Platform/AtomicInt.swift
Pods/RxSwift/Platform/DataStructures/Bag.swift
Pods/RxSwift/Platform/DataStructures/InfiniteSequence.swift
Pods/RxSwift/Platform/DataStructures/PriorityQueue.swift
Pods/RxSwift/Platform/DataStructures/Queue.swift
Pods/RxSwift/Platform/DispatchQueue+Extensions.swift
Pods/RxSwift/Platform/Platform.Darwin.swift
Pods/RxSwift/Platform/Platform.Linux.swift
Pods/RxSwift/Platform/RecursiveLock.swift
Pods/RxSwift/README.md
Pods/RxSwift/RxSwift/AnyObserver.swift
Pods/RxSwift/RxSwift/Binder.swift
Pods/RxSwift/RxSwift/Cancelable.swift
Pods/RxSwift/RxSwift/Concurrency/AsyncLock.swift
Pods/RxSwift/RxSwift/Concurrency/Lock.swift
Pods/RxSwift/RxSwift/Concurrency/LockOwnerType.swift
Pods/RxSwift/RxSwift/Concurrency/SynchronizedDisposeType.swift
Pods/RxSwift/RxSwift/Concurrency/SynchronizedOnType.swift
Pods/RxSwift/RxSwift/Concurrency/SynchronizedUnsubscribeType.swift
Pods/RxSwift/RxSwift/ConnectableObservableType.swift
Pods/RxSwift/RxSwift/Date+Dispatch.swift
Pods/RxSwift/RxSwift/Disposable.swift
Pods/RxSwift/RxSwift/Disposables/AnonymousDisposable.swift
Pods/RxSwift/RxSwift/Disposables/BinaryDisposable.swift
Pods/RxSwift/RxSwift/Disposables/BooleanDisposable.swift
Pods/RxSwift/RxSwift/Disposables/CompositeDisposable.swift
Pods/RxSwift/RxSwift/Disposables/Disposables.swift
Pods/RxSwift/RxSwift/Disposables/DisposeBag.swift
Pods/RxSwift/RxSwift/Disposables/DisposeBase.swift
Pods/RxSwift/RxSwift/Disposables/NopDisposable.swift
Pods/RxSwift/RxSwift/Disposables/RefCountDisposable.swift
Pods/RxSwift/RxSwift/Disposables/ScheduledDisposable.swift
Pods/RxSwift/RxSwift/Disposables/SerialDisposable.swift
Pods/RxSwift/RxSwift/Disposables/SingleAssignmentDisposable.swift
Pods/RxSwift/RxSwift/Disposables/SubscriptionDisposable.swift
Pods/RxSwift/RxSwift/Errors.swift
Pods/RxSwift/RxSwift/Event.swift
Pods/RxSwift/RxSwift/Extensions/Bag+Rx.swift
Pods/RxSwift/RxSwift/GroupedObservable.swift
Pods/RxSwift/RxSwift/ImmediateSchedulerType.swift
Pods/RxSwift/RxSwift/Observable+Concurrency.swift
Pods/RxSwift/RxSwift/Observable.swift
Pods/RxSwift/RxSwift/ObservableConvertibleType.swift
Pods/RxSwift/RxSwift/ObservableType+Extensions.swift
Pods/RxSwift/RxSwift/ObservableType.swift
Pods/RxSwift/RxSwift/Observables/AddRef.swift
Pods/RxSwift/RxSwift/Observables/Amb.swift
Pods/RxSwift/RxSwift/Observables/AsMaybe.swift
Pods/RxSwift/RxSwift/Observables/AsSingle.swift
Pods/RxSwift/RxSwift/Observables/Buffer.swift
Pods/RxSwift/RxSwift/Observables/Catch.swift
Pods/RxSwift/RxSwift/Observables/CombineLatest+Collection.swift
Pods/RxSwift/RxSwift/Observables/CombineLatest+arity.swift
Pods/RxSwift/RxSwift/Observables/CombineLatest.swift
Pods/RxSwift/RxSwift/Observables/CompactMap.swift
Pods/RxSwift/RxSwift/Observables/Concat.swift
Pods/RxSwift/RxSwift/Observables/Create.swift
Pods/RxSwift/RxSwift/Observables/Debounce.swift
Pods/RxSwift/RxSwift/Observables/Debug.swift
Pods/RxSwift/RxSwift/Observables/Decode.swift
Pods/RxSwift/RxSwift/Observables/DefaultIfEmpty.swift
Pods/RxSwift/RxSwift/Observables/Deferred.swift
Pods/RxSwift/RxSwift/Observables/Delay.swift
Pods/RxSwift/RxSwift/Observables/DelaySubscription.swift
Pods/RxSwift/RxSwift/Observables/Dematerialize.swift
Pods/RxSwift/RxSwift/Observables/DistinctUntilChanged.swift
Pods/RxSwift/RxSwift/Observables/Do.swift
Pods/RxSwift/RxSwift/Observables/ElementAt.swift
Pods/RxSwift/RxSwift/Observables/Empty.swift
Pods/RxSwift/RxSwift/Observables/Enumerated.swift
Pods/RxSwift/RxSwift/Observables/Error.swift
Pods/RxSwift/RxSwift/Observables/Filter.swift
Pods/RxSwift/RxSwift/Observables/First.swift
Pods/RxSwift/RxSwift/Observables/Generate.swift
Pods/RxSwift/RxSwift/Observables/GroupBy.swift
Pods/RxSwift/RxSwift/Observables/Just.swift
Pods/RxSwift/RxSwift/Observables/Map.swift
Pods/RxSwift/RxSwift/Observables/Materialize.swift
Pods/RxSwift/RxSwift/Observables/Merge.swift
Pods/RxSwift/RxSwift/Observables/Multicast.swift
Pods/RxSwift/RxSwift/Observables/Never.swift
Pods/RxSwift/RxSwift/Observables/ObserveOn.swift
Pods/RxSwift/RxSwift/Observables/Optional.swift
Pods/RxSwift/RxSwift/Observables/Producer.swift
Pods/RxSwift/RxSwift/Observables/Range.swift
Pods/RxSwift/RxSwift/Observables/Reduce.swift
Pods/RxSwift/RxSwift/Observables/Repeat.swift
Pods/RxSwift/RxSwift/Observables/RetryWhen.swift
Pods/RxSwift/RxSwift/Observables/Sample.swift
Pods/RxSwift/RxSwift/Observables/Scan.swift
Pods/RxSwift/RxSwift/Observables/Sequence.swift
Pods/RxSwift/RxSwift/Observables/ShareReplayScope.swift
Pods/RxSwift/RxSwift/Observables/SingleAsync.swift
Pods/RxSwift/RxSwift/Observables/Sink.swift
Pods/RxSwift/RxSwift/Observables/Skip.swift
Pods/RxSwift/RxSwift/Observables/SkipUntil.swift
Pods/RxSwift/RxSwift/Observables/SkipWhile.swift
Pods/RxSwift/RxSwift/Observables/StartWith.swift
Pods/RxSwift/RxSwift/Observables/SubscribeOn.swift
Pods/RxSwift/RxSwift/Observables/Switch.swift
Pods/RxSwift/RxSwift/Observables/SwitchIfEmpty.swift
Pods/RxSwift/RxSwift/Observables/Take.swift
Pods/RxSwift/RxSwift/Observables/TakeLast.swift
Pods/RxSwift/RxSwift/Observables/TakeWithPredicate.swift
Pods/RxSwift/RxSwift/Observables/Throttle.swift
Pods/RxSwift/RxSwift/Observables/Timeout.swift
Pods/RxSwift/RxSwift/Observables/Timer.swift
Pods/RxSwift/RxSwift/Observables/ToArray.swift
Pods/RxSwift/RxSwift/Observables/Using.swift
Pods/RxSwift/RxSwift/Observables/Window.swift
Pods/RxSwift/RxSwift/Observables/WithLatestFrom.swift
Pods/RxSwift/RxSwift/Observables/WithUnretained.swift
Pods/RxSwift/RxSwift/Observables/Zip+Collection.swift
Pods/RxSwift/RxSwift/Observables/Zip+arity.swift
Pods/RxSwift/RxSwift/Observables/Zip.swift
Pods/RxSwift/RxSwift/ObserverType.swift
Pods/RxSwift/RxSwift/Observers/AnonymousObserver.swift
Pods/RxSwift/RxSwift/Observers/ObserverBase.swift
Pods/RxSwift/RxSwift/Observers/TailRecursiveSink.swift
Pods/RxSwift/RxSwift/Reactive.swift
Pods/RxSwift/RxSwift/Rx.swift
Pods/RxSwift/RxSwift/RxMutableBox.swift
Pods/RxSwift/RxSwift/SchedulerType.swift
Pods/RxSwift/RxSwift/Schedulers/ConcurrentDispatchQueueScheduler.swift
Pods/RxSwift/RxSwift/Schedulers/ConcurrentMainScheduler.swift
Pods/RxSwift/RxSwift/Schedulers/CurrentThreadScheduler.swift
Pods/RxSwift/RxSwift/Schedulers/HistoricalScheduler.swift
Pods/RxSwift/RxSwift/Schedulers/HistoricalSchedulerTimeConverter.swift
Pods/RxSwift/RxSwift/Schedulers/Internal/DispatchQueueConfiguration.swift
Pods/RxSwift/RxSwift/Schedulers/Internal/InvocableScheduledItem.swift
Pods/RxSwift/RxSwift/Schedulers/Internal/InvocableType.swift
Pods/RxSwift/RxSwift/Schedulers/Internal/ScheduledItem.swift
Pods/RxSwift/RxSwift/Schedulers/Internal/ScheduledItemType.swift
Pods/RxSwift/RxSwift/Schedulers/MainScheduler.swift
Pods/RxSwift/RxSwift/Schedulers/OperationQueueScheduler.swift
Pods/RxSwift/RxSwift/Schedulers/RecursiveScheduler.swift
Pods/RxSwift/RxSwift/Schedulers/SchedulerServices+Emulation.swift
Pods/RxSwift/RxSwift/Schedulers/SerialDispatchQueueScheduler.swift
Pods/RxSwift/RxSwift/Schedulers/VirtualTimeConverterType.swift
Pods/RxSwift/RxSwift/Schedulers/VirtualTimeScheduler.swift
Pods/RxSwift/RxSwift/Subjects/AsyncSubject.swift
Pods/RxSwift/RxSwift/Subjects/BehaviorSubject.swift
Pods/RxSwift/RxSwift/Subjects/PublishSubject.swift
Pods/RxSwift/RxSwift/Subjects/ReplaySubject.swift
Pods/RxSwift/RxSwift/Subjects/SubjectType.swift
Pods/RxSwift/RxSwift/SwiftSupport/SwiftSupport.swift
Pods/RxSwift/RxSwift/Traits/Infallible/Infallible+CombineLatest+Collection.swift
Pods/RxSwift/RxSwift/Traits/Infallible/Infallible+CombineLatest+arity.swift
Pods/RxSwift/RxSwift/Traits/Infallible/Infallible+Concurrency.swift
Pods/RxSwift/RxSwift/Traits/Infallible/Infallible+Create.swift
Pods/RxSwift/RxSwift/Traits/Infallible/Infallible+Debug.swift
Pods/RxSwift/RxSwift/Traits/Infallible/Infallible+Operators.swift
Pods/RxSwift/RxSwift/Traits/Infallible/Infallible+Zip+arity.swift
Pods/RxSwift/RxSwift/Traits/Infallible/Infallible.swift
Pods/RxSwift/RxSwift/Traits/Infallible/ObservableConvertibleType+Infallible.swift
Pods/RxSwift/RxSwift/Traits/PrimitiveSequence/Completable+AndThen.swift
Pods/RxSwift/RxSwift/Traits/PrimitiveSequence/Completable.swift
Pods/RxSwift/RxSwift/Traits/PrimitiveSequence/Maybe.swift
Pods/RxSwift/RxSwift/Traits/PrimitiveSequence/ObservableType+PrimitiveSequence.swift
Pods/RxSwift/RxSwift/Traits/PrimitiveSequence/PrimitiveSequence+Concurrency.swift
Pods/RxSwift/RxSwift/Traits/PrimitiveSequence/PrimitiveSequence+Zip+arity.swift
Pods/RxSwift/RxSwift/Traits/PrimitiveSequence/PrimitiveSequence.swift
Pods/RxSwift/RxSwift/Traits/PrimitiveSequence/Single.swift
Pods/SDWebImage/LICENSE
Pods/SDWebImage/README.md
Pods/SDWebImage/SDWebImage/Core/NSButton+WebCache.h
Pods/SDWebImage/SDWebImage/Core/NSButton+WebCache.m
Pods/SDWebImage/SDWebImage/Core/NSData+ImageContentType.h
Pods/SDWebImage/SDWebImage/Core/NSData+ImageContentType.m
Pods/SDWebImage/SDWebImage/Core/NSImage+Compatibility.h
Pods/SDWebImage/SDWebImage/Core/NSImage+Compatibility.m
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImage.h
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImage.m
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImagePlayer.h
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImagePlayer.m
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageRep.h
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageRep.m
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView+WebCache.h
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView+WebCache.m
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView.h
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView.m
Pods/SDWebImage/SDWebImage/Core/SDCallbackQueue.h
Pods/SDWebImage/SDWebImage/Core/SDCallbackQueue.m
Pods/SDWebImage/SDWebImage/Core/SDDiskCache.h
Pods/SDWebImage/SDWebImage/Core/SDDiskCache.m
Pods/SDWebImage/SDWebImage/Core/SDGraphicsImageRenderer.h
Pods/SDWebImage/SDWebImage/Core/SDGraphicsImageRenderer.m
Pods/SDWebImage/SDWebImage/Core/SDImageAPNGCoder.h
Pods/SDWebImage/SDWebImage/Core/SDImageAPNGCoder.m
Pods/SDWebImage/SDWebImage/Core/SDImageAWebPCoder.h
Pods/SDWebImage/SDWebImage/Core/SDImageAWebPCoder.m
Pods/SDWebImage/SDWebImage/Core/SDImageCache.h
Pods/SDWebImage/SDWebImage/Core/SDImageCache.m
Pods/SDWebImage/SDWebImage/Core/SDImageCacheConfig.h
Pods/SDWebImage/SDWebImage/Core/SDImageCacheConfig.m
Pods/SDWebImage/SDWebImage/Core/SDImageCacheDefine.h
Pods/SDWebImage/SDWebImage/Core/SDImageCacheDefine.m
Pods/SDWebImage/SDWebImage/Core/SDImageCachesManager.h
Pods/SDWebImage/SDWebImage/Core/SDImageCachesManager.m
Pods/SDWebImage/SDWebImage/Core/SDImageCoder.h
Pods/SDWebImage/SDWebImage/Core/SDImageCoder.m
Pods/SDWebImage/SDWebImage/Core/SDImageCoderHelper.h
Pods/SDWebImage/SDWebImage/Core/SDImageCoderHelper.m
Pods/SDWebImage/SDWebImage/Core/SDImageCodersManager.h
Pods/SDWebImage/SDWebImage/Core/SDImageCodersManager.m
Pods/SDWebImage/SDWebImage/Core/SDImageFrame.h
Pods/SDWebImage/SDWebImage/Core/SDImageFrame.m
Pods/SDWebImage/SDWebImage/Core/SDImageGIFCoder.h
Pods/SDWebImage/SDWebImage/Core/SDImageGIFCoder.m
Pods/SDWebImage/SDWebImage/Core/SDImageGraphics.h
Pods/SDWebImage/SDWebImage/Core/SDImageGraphics.m
Pods/SDWebImage/SDWebImage/Core/SDImageHEICCoder.h
Pods/SDWebImage/SDWebImage/Core/SDImageHEICCoder.m
Pods/SDWebImage/SDWebImage/Core/SDImageIOAnimatedCoder.h
Pods/SDWebImage/SDWebImage/Core/SDImageIOAnimatedCoder.m
Pods/SDWebImage/SDWebImage/Core/SDImageIOCoder.h
Pods/SDWebImage/SDWebImage/Core/SDImageIOCoder.m
Pods/SDWebImage/SDWebImage/Core/SDImageLoader.h
Pods/SDWebImage/SDWebImage/Core/SDImageLoader.m
Pods/SDWebImage/SDWebImage/Core/SDImageLoadersManager.h
Pods/SDWebImage/SDWebImage/Core/SDImageLoadersManager.m
Pods/SDWebImage/SDWebImage/Core/SDImageTransformer.h
Pods/SDWebImage/SDWebImage/Core/SDImageTransformer.m
Pods/SDWebImage/SDWebImage/Core/SDMemoryCache.h
Pods/SDWebImage/SDWebImage/Core/SDMemoryCache.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageCacheKeyFilter.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageCacheKeyFilter.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageCacheSerializer.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageCacheSerializer.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageCompat.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageCompat.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageDefine.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageDefine.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloader.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloader.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderConfig.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderConfig.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderDecryptor.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderDecryptor.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderOperation.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderOperation.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderRequestModifier.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderRequestModifier.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderResponseModifier.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderResponseModifier.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageError.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageError.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageIndicator.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageIndicator.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageManager.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageManager.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageOperation.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageOperation.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageOptionsProcessor.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageOptionsProcessor.m
Pods/SDWebImage/SDWebImage/Core/SDWebImagePrefetcher.h
Pods/SDWebImage/SDWebImage/Core/SDWebImagePrefetcher.m
Pods/SDWebImage/SDWebImage/Core/SDWebImageTransition.h
Pods/SDWebImage/SDWebImage/Core/SDWebImageTransition.m
Pods/SDWebImage/SDWebImage/Core/UIButton+WebCache.h
Pods/SDWebImage/SDWebImage/Core/UIButton+WebCache.m
Pods/SDWebImage/SDWebImage/Core/UIImage+ExtendedCacheData.h
Pods/SDWebImage/SDWebImage/Core/UIImage+ExtendedCacheData.m
Pods/SDWebImage/SDWebImage/Core/UIImage+ForceDecode.h
Pods/SDWebImage/SDWebImage/Core/UIImage+ForceDecode.m
Pods/SDWebImage/SDWebImage/Core/UIImage+GIF.h
Pods/SDWebImage/SDWebImage/Core/UIImage+GIF.m
Pods/SDWebImage/SDWebImage/Core/UIImage+MemoryCacheCost.h
Pods/SDWebImage/SDWebImage/Core/UIImage+MemoryCacheCost.m
Pods/SDWebImage/SDWebImage/Core/UIImage+Metadata.h
Pods/SDWebImage/SDWebImage/Core/UIImage+Metadata.m
Pods/SDWebImage/SDWebImage/Core/UIImage+MultiFormat.h
Pods/SDWebImage/SDWebImage/Core/UIImage+MultiFormat.m
Pods/SDWebImage/SDWebImage/Core/UIImage+Transform.h
Pods/SDWebImage/SDWebImage/Core/UIImage+Transform.m
Pods/SDWebImage/SDWebImage/Core/UIImageView+HighlightedWebCache.h
Pods/SDWebImage/SDWebImage/Core/UIImageView+HighlightedWebCache.m
Pods/SDWebImage/SDWebImage/Core/UIImageView+WebCache.h
Pods/SDWebImage/SDWebImage/Core/UIImageView+WebCache.m
Pods/SDWebImage/SDWebImage/Core/UIView+WebCache.h
Pods/SDWebImage/SDWebImage/Core/UIView+WebCache.m
Pods/SDWebImage/SDWebImage/Core/UIView+WebCacheOperation.h
Pods/SDWebImage/SDWebImage/Core/UIView+WebCacheOperation.m
Pods/SDWebImage/SDWebImage/Core/UIView+WebCacheState.h
Pods/SDWebImage/SDWebImage/Core/UIView+WebCacheState.m
Pods/SDWebImage/SDWebImage/Private/NSBezierPath+SDRoundedCorners.h
Pods/SDWebImage/SDWebImage/Private/NSBezierPath+SDRoundedCorners.m
Pods/SDWebImage/SDWebImage/Private/SDAssociatedObject.h
Pods/SDWebImage/SDWebImage/Private/SDAssociatedObject.m
Pods/SDWebImage/SDWebImage/Private/SDAsyncBlockOperation.h
Pods/SDWebImage/SDWebImage/Private/SDAsyncBlockOperation.m
Pods/SDWebImage/SDWebImage/Private/SDDeviceHelper.h
Pods/SDWebImage/SDWebImage/Private/SDDeviceHelper.m
Pods/SDWebImage/SDWebImage/Private/SDDisplayLink.h
Pods/SDWebImage/SDWebImage/Private/SDDisplayLink.m
Pods/SDWebImage/SDWebImage/Private/SDFileAttributeHelper.h
Pods/SDWebImage/SDWebImage/Private/SDFileAttributeHelper.m
Pods/SDWebImage/SDWebImage/Private/SDImageAssetManager.h
Pods/SDWebImage/SDWebImage/Private/SDImageAssetManager.m
Pods/SDWebImage/SDWebImage/Private/SDImageCachesManagerOperation.h
Pods/SDWebImage/SDWebImage/Private/SDImageCachesManagerOperation.m
Pods/SDWebImage/SDWebImage/Private/SDImageFramePool.h
Pods/SDWebImage/SDWebImage/Private/SDImageFramePool.m
Pods/SDWebImage/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h
Pods/SDWebImage/SDWebImage/Private/SDInternalMacros.h
Pods/SDWebImage/SDWebImage/Private/SDInternalMacros.m
Pods/SDWebImage/SDWebImage/Private/SDWeakProxy.h
Pods/SDWebImage/SDWebImage/Private/SDWeakProxy.m
Pods/SDWebImage/SDWebImage/Private/SDWebImageTransitionInternal.h
Pods/SDWebImage/SDWebImage/Private/SDmetamacros.h
Pods/SDWebImage/SDWebImage/Private/UIColor+SDHexString.h
Pods/SDWebImage/SDWebImage/Private/UIColor+SDHexString.m
Pods/SDWebImage/WebImage/SDWebImage.h
Pods/SPPageMenu/README.md
Pods/SPPageMenu/SPPageMenu/SPPageMenu.h
Pods/SPPageMenu/SPPageMenu/SPPageMenu.m
Pods/SVProgressHUD/LICENSE
Pods/SVProgressHUD/README.md
Pods/SVProgressHUD/SVProgressHUD/PrivacyInfo.xcprivacy
Pods/SVProgressHUD/SVProgressHUD/SVIndefiniteAnimatedView.h
Pods/SVProgressHUD/SVProgressHUD/SVIndefiniteAnimatedView.m
Pods/SVProgressHUD/SVProgressHUD/SVProgressAnimatedView.h
Pods/SVProgressHUD/SVProgressHUD/SVProgressAnimatedView.m
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask.png
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask@2x.png
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask@3x.png
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error.png
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error@2x.png
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error@3x.png
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info.png
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info@2x.png
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info@3x.png
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success.png
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success@2x.png
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success@3x.png
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.h
Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.m
Pods/SVProgressHUD/SVProgressHUD/SVRadialGradientLayer.h
Pods/SVProgressHUD/SVProgressHUD/SVRadialGradientLayer.m
Pods/SnapKit/LICENSE
Pods/SnapKit/README.md
Pods/SnapKit/Sources/Constraint.swift
Pods/SnapKit/Sources/ConstraintAttributes.swift
Pods/SnapKit/Sources/ConstraintConfig.swift
Pods/SnapKit/Sources/ConstraintConstantTarget.swift
Pods/SnapKit/Sources/ConstraintDSL.swift
Pods/SnapKit/Sources/ConstraintDescription.swift
Pods/SnapKit/Sources/ConstraintDirectionalInsetTarget.swift
Pods/SnapKit/Sources/ConstraintDirectionalInsets.swift
Pods/SnapKit/Sources/ConstraintInsetTarget.swift
Pods/SnapKit/Sources/ConstraintInsets.swift
Pods/SnapKit/Sources/ConstraintItem.swift
Pods/SnapKit/Sources/ConstraintLayoutGuide+Extensions.swift
Pods/SnapKit/Sources/ConstraintLayoutGuide.swift
Pods/SnapKit/Sources/ConstraintLayoutGuideDSL.swift
Pods/SnapKit/Sources/ConstraintLayoutSupport.swift
Pods/SnapKit/Sources/ConstraintLayoutSupportDSL.swift
Pods/SnapKit/Sources/ConstraintMaker.swift
Pods/SnapKit/Sources/ConstraintMakerEditable.swift
Pods/SnapKit/Sources/ConstraintMakerExtendable.swift
Pods/SnapKit/Sources/ConstraintMakerFinalizable.swift
Pods/SnapKit/Sources/ConstraintMakerPrioritizable.swift
Pods/SnapKit/Sources/ConstraintMakerRelatable+Extensions.swift
Pods/SnapKit/Sources/ConstraintMakerRelatable.swift
Pods/SnapKit/Sources/ConstraintMultiplierTarget.swift
Pods/SnapKit/Sources/ConstraintOffsetTarget.swift
Pods/SnapKit/Sources/ConstraintPriority.swift
Pods/SnapKit/Sources/ConstraintPriorityTarget.swift
Pods/SnapKit/Sources/ConstraintRelatableTarget.swift
Pods/SnapKit/Sources/ConstraintRelation.swift
Pods/SnapKit/Sources/ConstraintView+Extensions.swift
Pods/SnapKit/Sources/ConstraintView.swift
Pods/SnapKit/Sources/ConstraintViewDSL.swift
Pods/SnapKit/Sources/Debugging.swift
Pods/SnapKit/Sources/LayoutConstraint.swift
Pods/SnapKit/Sources/LayoutConstraintItem.swift
Pods/SnapKit/Sources/Typealiases.swift
Pods/SnapKit/Sources/UILayoutSupport+Extensions.swift
Pods/SwifterSwift/LICENSE
Pods/SwifterSwift/README.md
Pods/SwifterSwift/Sources/SwifterSwift/AppKit/NSColorExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/AppKit/NSImageExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/AppKit/NSViewExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Combine/FutureExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/CoreAnimation/CAGradientLayerExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/CoreAnimation/CATransform3DExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/CoreGraphics/CGAffineTransformExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/CoreGraphics/CGColorExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/CoreGraphics/CGFloatExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/CoreGraphics/CGPointExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/CoreGraphics/CGRectExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/CoreGraphics/CGSizeExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/CoreGraphics/CGVectorExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/CoreLocation/CLLocationArrayExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/CoreLocation/CLLocationExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/CoreLocation/CLVisitExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/CryptoKit/DigestExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Dispatch/DispatchQueueExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/CalendarExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/DataExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/DateExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/FileManagerExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/LocaleExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/MeasurementExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/NSAttributedStringExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/NSPredicateExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/NSRegularExpressionExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/NotificationCenterExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/URLExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/URLRequestExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/URLSessionExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Foundation/UserDefaultsExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/HealthKit/HKActivitySummaryExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/MapKit/MKMapViewExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/MapKit/MKMultiPointExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/MapKit/MKPolylineExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SceneKit/SCNBoxExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SceneKit/SCNCapsuleExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SceneKit/SCNConeExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SceneKit/SCNCylinderExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SceneKit/SCNGeometryExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SceneKit/SCNMaterialExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SceneKit/SCNPlaneExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SceneKit/SCNShapeExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SceneKit/SCNSphereExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SceneKit/SCNVector3Extensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Shared/ColorExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Shared/EdgeInsetsExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/Shared/FontExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SpriteKit/SKNodeExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SpriteKit/SKSpriteNodeExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/StoreKit/SKProductExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/ArrayExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/BidirectionalCollectionExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/BinaryFloatingPointExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/BinaryIntegerExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/BoolExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/CharacterExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/CollectionExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/ComparableExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/DecodableExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/DefaultStringInterpolationExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/DictionaryExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/DoubleExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/FloatExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/FloatingPointExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/IntExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/KeyedDecodingContainerExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/MutableCollectionExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/OptionalExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/RangeReplaceableCollectionExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/SequenceExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/SignedIntegerExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/SignedNumericExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/StringExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/SwiftStdlib/StringProtocolExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIActivityExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIAlertControllerExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIApplicationExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIBarButtonItemExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIBezierPathExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIButtonExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UICollectionViewExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIColorExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIFontExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIGestureRecognizerExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIImageExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIImageViewExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UILabelExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UILayoutPriorityExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UINavigationBarExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UINavigationControllerExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UINavigationItemExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIRefreshControlExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIScrollViewExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UISearchBarExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UISegmentedControlExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UISliderExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIStackViewExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIStoryboardExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UISwitchExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UITabBarExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UITableViewExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UITextFieldExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UITextViewExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIViewControllerExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIViewExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/UIKit/UIWindowExtensions.swift
Pods/SwifterSwift/Sources/SwifterSwift/WebKit/WKWebViewExtensions.swift
Pods/SwiftyStoreKit/LICENSE.md
Pods/SwiftyStoreKit/README.md
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/AppleReceiptValidator.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/CompleteTransactionsController.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/InAppProductQueryRequest.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/InAppReceipt.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/InAppReceiptRefreshRequest.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/InAppReceiptVerificator.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/OS.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/PaymentQueueController.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/PaymentsController.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/ProductsInfoController.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/RestorePurchasesController.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/SKProduct+LocalizedPrice.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/SKProductDiscount+LocalizedPrice.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/SwiftyStoreKit+Types.swift
Pods/SwiftyStoreKit/Sources/SwiftyStoreKit/SwiftyStoreKit.swift
Pods/TZImagePickerController/LICENSE
Pods/TZImagePickerController/README.md
Pods/TZImagePickerController/TZImagePickerController/Location/TZLocationManager.h
Pods/TZImagePickerController/TZImagePickerController/Location/TZLocationManager.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/NSBundle+TZImagePicker.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/NSBundle+TZImagePicker.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZAssetCell.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZAssetCell.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZAssetModel.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZAssetModel.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZAuthLimitedFooterTipView.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZAuthLimitedFooterTipView.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZGifPhotoPreviewController.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZGifPhotoPreviewController.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImageCropManager.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImageCropManager.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImageManager.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImageManager.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/MMVideoPreviewPlay@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/MMVideoPreviewPlayHL@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/VideoSendIcon@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/addMore@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/ar.lproj/Localizable.strings
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/de.lproj/Localizable.strings
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/en.lproj/Localizable.strings
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/es.lproj/Localizable.strings
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/fr.lproj/Localizable.strings
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/iCloudError@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/ja.lproj/Localizable.strings
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/ko-KP.lproj/Localizable.strings
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/navi_back@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_def_photoPickerVc@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_def_previewVc@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_number_icon@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_original_def@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_original_sel@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_sel_photoPickerVc@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/photo_sel_previewVc@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/preview_number_icon@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/preview_original_def@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/pt.lproj/Localizable.strings
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/right_arrow@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/ru.lproj/Localizable.strings
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/takePicture80@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/takePicture@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/tip@2x.png
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/vi.lproj/Localizable.strings
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/zh-Hans.lproj/Localizable.strings
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.bundle/zh-Hant.lproj/Localizable.strings
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImagePickerController.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImageRequestOperation.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZImageRequestOperation.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZPhotoPickerController.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZPhotoPickerController.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZPhotoPreviewCell.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZPhotoPreviewCell.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZPhotoPreviewController.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZPhotoPreviewController.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZProgressView.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZProgressView.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZVideoCropController.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZVideoCropController.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZVideoEditedPreviewController.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZVideoEditedPreviewController.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZVideoPlayerController.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/TZVideoPlayerController.m
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/UIView+TZLayout.h
Pods/TZImagePickerController/TZImagePickerController/TZImagePickerController/UIView+TZLayout.m
Pods/Target Support Files/Alamofire/Alamofire-Info.plist
Pods/Target Support Files/Alamofire/Alamofire-dummy.m
Pods/Target Support Files/Alamofire/Alamofire-prefix.pch
Pods/Target Support Files/Alamofire/Alamofire-umbrella.h
Pods/Target Support Files/Alamofire/Alamofire.debug.xcconfig
Pods/Target Support Files/Alamofire/Alamofire.modulemap
Pods/Target Support Files/Alamofire/Alamofire.release.xcconfig
Pods/Target Support Files/CryptoSwift/CryptoSwift-Info.plist
Pods/Target Support Files/CryptoSwift/CryptoSwift-dummy.m
Pods/Target Support Files/CryptoSwift/CryptoSwift-prefix.pch
Pods/Target Support Files/CryptoSwift/CryptoSwift-umbrella.h
Pods/Target Support Files/CryptoSwift/CryptoSwift.debug.xcconfig
Pods/Target Support Files/CryptoSwift/CryptoSwift.modulemap
Pods/Target Support Files/CryptoSwift/CryptoSwift.release.xcconfig
Pods/Target Support Files/Differentiator/Differentiator-Info.plist
Pods/Target Support Files/Differentiator/Differentiator-dummy.m
Pods/Target Support Files/Differentiator/Differentiator-prefix.pch
Pods/Target Support Files/Differentiator/Differentiator-umbrella.h
Pods/Target Support Files/Differentiator/Differentiator.debug.xcconfig
Pods/Target Support Files/Differentiator/Differentiator.modulemap
Pods/Target Support Files/Differentiator/Differentiator.release.xcconfig
Pods/Target Support Files/EmptyDataSet-Swift/EmptyDataSet-Swift-Info.plist
Pods/Target Support Files/EmptyDataSet-Swift/EmptyDataSet-Swift-dummy.m
Pods/Target Support Files/EmptyDataSet-Swift/EmptyDataSet-Swift-prefix.pch
Pods/Target Support Files/EmptyDataSet-Swift/EmptyDataSet-Swift-umbrella.h
Pods/Target Support Files/EmptyDataSet-Swift/EmptyDataSet-Swift.debug.xcconfig
Pods/Target Support Files/EmptyDataSet-Swift/EmptyDataSet-Swift.modulemap
Pods/Target Support Files/EmptyDataSet-Swift/EmptyDataSet-Swift.release.xcconfig
Pods/Target Support Files/FFPage/FFPage-Info.plist
Pods/Target Support Files/FFPage/FFPage-dummy.m
Pods/Target Support Files/FFPage/FFPage-prefix.pch
Pods/Target Support Files/FFPage/FFPage-umbrella.h
Pods/Target Support Files/FFPage/FFPage.debug.xcconfig
Pods/Target Support Files/FFPage/FFPage.modulemap
Pods/Target Support Files/FFPage/FFPage.release.xcconfig
Pods/Target Support Files/HandyJSON/HandyJSON-Info.plist
Pods/Target Support Files/HandyJSON/HandyJSON-dummy.m
Pods/Target Support Files/HandyJSON/HandyJSON-prefix.pch
Pods/Target Support Files/HandyJSON/HandyJSON-umbrella.h
Pods/Target Support Files/HandyJSON/HandyJSON.debug.xcconfig
Pods/Target Support Files/HandyJSON/HandyJSON.modulemap
Pods/Target Support Files/HandyJSON/HandyJSON.release.xcconfig
Pods/Target Support Files/IQKeyboardManager/IQKeyboardManager-Info.plist
Pods/Target Support Files/IQKeyboardManager/IQKeyboardManager-dummy.m
Pods/Target Support Files/IQKeyboardManager/IQKeyboardManager-prefix.pch
Pods/Target Support Files/IQKeyboardManager/IQKeyboardManager-umbrella.h
Pods/Target Support Files/IQKeyboardManager/IQKeyboardManager.debug.xcconfig
Pods/Target Support Files/IQKeyboardManager/IQKeyboardManager.modulemap
Pods/Target Support Files/IQKeyboardManager/IQKeyboardManager.release.xcconfig
Pods/Target Support Files/IQKeyboardManagerSwift/IQKeyboardManagerSwift-Info.plist
Pods/Target Support Files/IQKeyboardManagerSwift/IQKeyboardManagerSwift-dummy.m
Pods/Target Support Files/IQKeyboardManagerSwift/IQKeyboardManagerSwift-prefix.pch
Pods/Target Support Files/IQKeyboardManagerSwift/IQKeyboardManagerSwift-umbrella.h
Pods/Target Support Files/IQKeyboardManagerSwift/IQKeyboardManagerSwift.debug.xcconfig
Pods/Target Support Files/IQKeyboardManagerSwift/IQKeyboardManagerSwift.modulemap
Pods/Target Support Files/IQKeyboardManagerSwift/IQKeyboardManagerSwift.release.xcconfig
Pods/Target Support Files/JQTools/JQTools-Info.plist
Pods/Target Support Files/JQTools/JQTools-dummy.m
Pods/Target Support Files/JQTools/JQTools-prefix.pch
Pods/Target Support Files/JQTools/JQTools-umbrella.h
Pods/Target Support Files/JQTools/JQTools.debug.xcconfig
Pods/Target Support Files/JQTools/JQTools.modulemap
Pods/Target Support Files/JQTools/JQTools.release.xcconfig
Pods/Target Support Files/JQTools/ResourceBundle-JQToolsRes-JQTools-Info.plist
Pods/Target Support Files/Lantern/Lantern-Info.plist
Pods/Target Support Files/Lantern/Lantern-dummy.m
Pods/Target Support Files/Lantern/Lantern-prefix.pch
Pods/Target Support Files/Lantern/Lantern-umbrella.h
Pods/Target Support Files/Lantern/Lantern.debug.xcconfig
Pods/Target Support Files/Lantern/Lantern.modulemap
Pods/Target Support Files/Lantern/Lantern.release.xcconfig
Pods/Target Support Files/MJRefresh/MJRefresh-Info.plist
Pods/Target Support Files/MJRefresh/MJRefresh-dummy.m
Pods/Target Support Files/MJRefresh/MJRefresh-prefix.pch
Pods/Target Support Files/MJRefresh/MJRefresh-umbrella.h
Pods/Target Support Files/MJRefresh/MJRefresh.debug.xcconfig
Pods/Target Support Files/MJRefresh/MJRefresh.modulemap
Pods/Target Support Files/MJRefresh/MJRefresh.release.xcconfig
Pods/Target Support Files/ObjcExceptionBridging/ObjcExceptionBridging-Info.plist
Pods/Target Support Files/ObjcExceptionBridging/ObjcExceptionBridging-dummy.m
Pods/Target Support Files/ObjcExceptionBridging/ObjcExceptionBridging-prefix.pch
Pods/Target Support Files/ObjcExceptionBridging/ObjcExceptionBridging-umbrella.h
Pods/Target Support Files/ObjcExceptionBridging/ObjcExceptionBridging.debug.xcconfig
Pods/Target Support Files/ObjcExceptionBridging/ObjcExceptionBridging.modulemap
Pods/Target Support Files/ObjcExceptionBridging/ObjcExceptionBridging.release.xcconfig
Pods/Target Support Files/ObjectMapper/ObjectMapper-Info.plist
Pods/Target Support Files/ObjectMapper/ObjectMapper-dummy.m
Pods/Target Support Files/ObjectMapper/ObjectMapper-prefix.pch
Pods/Target Support Files/ObjectMapper/ObjectMapper-umbrella.h
Pods/Target Support Files/ObjectMapper/ObjectMapper.debug.xcconfig
Pods/Target Support Files/ObjectMapper/ObjectMapper.modulemap
Pods/Target Support Files/ObjectMapper/ObjectMapper.release.xcconfig
Pods/Target Support Files/Pods-DolphinEnglishLearnStudent/Pods-DolphinEnglishLearnStudent-Info.plist
Pods/Target Support Files/Pods-DolphinEnglishLearnStudent/Pods-DolphinEnglishLearnStudent-acknowledgements.markdown
Pods/Target Support Files/Pods-DolphinEnglishLearnStudent/Pods-DolphinEnglishLearnStudent-acknowledgements.plist
Pods/Target Support Files/Pods-DolphinEnglishLearnStudent/Pods-DolphinEnglishLearnStudent-dummy.m
Pods/Target Support Files/Pods-DolphinEnglishLearnStudent/Pods-DolphinEnglishLearnStudent-frameworks-Debug-input-files.xcfilelist
Pods/Target Support Files/Pods-DolphinEnglishLearnStudent/Pods-DolphinEnglishLearnStudent-frameworks-Debug-output-files.xcfilelist
Pods/Target Support Files/Pods-DolphinEnglishLearnStudent/Pods-DolphinEnglishLearnStudent-frameworks-Release-input-files.xcfilelist
Pods/Target Support Files/Pods-DolphinEnglishLearnStudent/Pods-DolphinEnglishLearnStudent-frameworks-Release-output-files.xcfilelist
Pods/Target Support Files/Pods-DolphinEnglishLearnStudent/Pods-DolphinEnglishLearnStudent-frameworks.sh
Pods/Target Support Files/Pods-DolphinEnglishLearnStudent/Pods-DolphinEnglishLearnStudent-umbrella.h
Pods/Target Support Files/Pods-DolphinEnglishLearnStudent/Pods-DolphinEnglishLearnStudent.debug.xcconfig
Pods/Target Support Files/Pods-DolphinEnglishLearnStudent/Pods-DolphinEnglishLearnStudent.modulemap
Pods/Target Support Files/Pods-DolphinEnglishLearnStudent/Pods-DolphinEnglishLearnStudent.release.xcconfig
Pods/Target Support Files/QMUIKit/QMUIKit-Info.plist
Pods/Target Support Files/QMUIKit/QMUIKit-dummy.m
Pods/Target Support Files/QMUIKit/QMUIKit-prefix.pch
Pods/Target Support Files/QMUIKit/QMUIKit-umbrella.h
Pods/Target Support Files/QMUIKit/QMUIKit.debug.xcconfig
Pods/Target Support Files/QMUIKit/QMUIKit.modulemap
Pods/Target Support Files/QMUIKit/QMUIKit.release.xcconfig
Pods/Target Support Files/QMUIKit/ResourceBundle-QMUIResources-QMUIKit-Info.plist
Pods/Target Support Files/RxCocoa/RxCocoa-Info.plist
Pods/Target Support Files/RxCocoa/RxCocoa-dummy.m
Pods/Target Support Files/RxCocoa/RxCocoa-prefix.pch
Pods/Target Support Files/RxCocoa/RxCocoa-umbrella.h
Pods/Target Support Files/RxCocoa/RxCocoa.debug.xcconfig
Pods/Target Support Files/RxCocoa/RxCocoa.modulemap
Pods/Target Support Files/RxCocoa/RxCocoa.release.xcconfig
Pods/Target Support Files/RxDataSources/RxDataSources-Info.plist
Pods/Target Support Files/RxDataSources/RxDataSources-dummy.m
Pods/Target Support Files/RxDataSources/RxDataSources-prefix.pch
Pods/Target Support Files/RxDataSources/RxDataSources-umbrella.h
Pods/Target Support Files/RxDataSources/RxDataSources.debug.xcconfig
Pods/Target Support Files/RxDataSources/RxDataSources.modulemap
Pods/Target Support Files/RxDataSources/RxDataSources.release.xcconfig
Pods/Target Support Files/RxRelay/RxRelay-Info.plist
Pods/Target Support Files/RxRelay/RxRelay-dummy.m
Pods/Target Support Files/RxRelay/RxRelay-prefix.pch
Pods/Target Support Files/RxRelay/RxRelay-umbrella.h
Pods/Target Support Files/RxRelay/RxRelay.debug.xcconfig
Pods/Target Support Files/RxRelay/RxRelay.modulemap
Pods/Target Support Files/RxRelay/RxRelay.release.xcconfig
Pods/Target Support Files/RxSwift/RxSwift-Info.plist
Pods/Target Support Files/RxSwift/RxSwift-dummy.m
Pods/Target Support Files/RxSwift/RxSwift-prefix.pch
Pods/Target Support Files/RxSwift/RxSwift-umbrella.h
Pods/Target Support Files/RxSwift/RxSwift.debug.xcconfig
Pods/Target Support Files/RxSwift/RxSwift.modulemap
Pods/Target Support Files/RxSwift/RxSwift.release.xcconfig
Pods/Target Support Files/SDWebImage/SDWebImage-Info.plist
Pods/Target Support Files/SDWebImage/SDWebImage-dummy.m
Pods/Target Support Files/SDWebImage/SDWebImage-prefix.pch
Pods/Target Support Files/SDWebImage/SDWebImage-umbrella.h
Pods/Target Support Files/SDWebImage/SDWebImage.debug.xcconfig
Pods/Target Support Files/SDWebImage/SDWebImage.modulemap
Pods/Target Support Files/SDWebImage/SDWebImage.release.xcconfig
Pods/Target Support Files/SPPageMenu/SPPageMenu-Info.plist
Pods/Target Support Files/SPPageMenu/SPPageMenu-dummy.m
Pods/Target Support Files/SPPageMenu/SPPageMenu-prefix.pch
Pods/Target Support Files/SPPageMenu/SPPageMenu-umbrella.h
Pods/Target Support Files/SPPageMenu/SPPageMenu.debug.xcconfig
Pods/Target Support Files/SPPageMenu/SPPageMenu.modulemap
Pods/Target Support Files/SPPageMenu/SPPageMenu.release.xcconfig
Pods/Target Support Files/SVProgressHUD/SVProgressHUD-Info.plist
Pods/Target Support Files/SVProgressHUD/SVProgressHUD-dummy.m
Pods/Target Support Files/SVProgressHUD/SVProgressHUD-prefix.pch
Pods/Target Support Files/SVProgressHUD/SVProgressHUD-umbrella.h
Pods/Target Support Files/SVProgressHUD/SVProgressHUD.debug.xcconfig
Pods/Target Support Files/SVProgressHUD/SVProgressHUD.modulemap
Pods/Target Support Files/SVProgressHUD/SVProgressHUD.release.xcconfig
Pods/Target Support Files/SnapKit/SnapKit-Info.plist
Pods/Target Support Files/SnapKit/SnapKit-dummy.m
Pods/Target Support Files/SnapKit/SnapKit-prefix.pch
Pods/Target Support Files/SnapKit/SnapKit-umbrella.h
Pods/Target Support Files/SnapKit/SnapKit.debug.xcconfig
Pods/Target Support Files/SnapKit/SnapKit.modulemap
Pods/Target Support Files/SnapKit/SnapKit.release.xcconfig
Pods/Target Support Files/SwifterSwift/SwifterSwift-Info.plist
Pods/Target Support Files/SwifterSwift/SwifterSwift-dummy.m
Pods/Target Support Files/SwifterSwift/SwifterSwift-prefix.pch
Pods/Target Support Files/SwifterSwift/SwifterSwift-umbrella.h
Pods/Target Support Files/SwifterSwift/SwifterSwift.debug.xcconfig
Pods/Target Support Files/SwifterSwift/SwifterSwift.modulemap
Pods/Target Support Files/SwifterSwift/SwifterSwift.release.xcconfig
Pods/Target Support Files/SwiftyStoreKit/SwiftyStoreKit-Info.plist
Pods/Target Support Files/SwiftyStoreKit/SwiftyStoreKit-dummy.m
Pods/Target Support Files/SwiftyStoreKit/SwiftyStoreKit-prefix.pch
Pods/Target Support Files/SwiftyStoreKit/SwiftyStoreKit-umbrella.h
Pods/Target Support Files/SwiftyStoreKit/SwiftyStoreKit.debug.xcconfig
Pods/Target Support Files/SwiftyStoreKit/SwiftyStoreKit.modulemap
Pods/Target Support Files/SwiftyStoreKit/SwiftyStoreKit.release.xcconfig
Pods/Target Support Files/TZImagePickerController/TZImagePickerController-Info.plist
Pods/Target Support Files/TZImagePickerController/TZImagePickerController-dummy.m
Pods/Target Support Files/TZImagePickerController/TZImagePickerController-prefix.pch
Pods/Target Support Files/TZImagePickerController/TZImagePickerController-umbrella.h
Pods/Target Support Files/TZImagePickerController/TZImagePickerController.debug.xcconfig
Pods/Target Support Files/TZImagePickerController/TZImagePickerController.modulemap
Pods/Target Support Files/TZImagePickerController/TZImagePickerController.release.xcconfig
Pods/Target Support Files/UserDefaultsStore/UserDefaultsStore-Info.plist
Pods/Target Support Files/UserDefaultsStore/UserDefaultsStore-dummy.m
Pods/Target Support Files/UserDefaultsStore/UserDefaultsStore-prefix.pch
Pods/Target Support Files/UserDefaultsStore/UserDefaultsStore-umbrella.h
Pods/Target Support Files/UserDefaultsStore/UserDefaultsStore.debug.xcconfig
Pods/Target Support Files/UserDefaultsStore/UserDefaultsStore.modulemap
Pods/Target Support Files/UserDefaultsStore/UserDefaultsStore.release.xcconfig
Pods/Target Support Files/VTMagic/VTMagic-Info.plist
Pods/Target Support Files/VTMagic/VTMagic-dummy.m
Pods/Target Support Files/VTMagic/VTMagic-prefix.pch
Pods/Target Support Files/VTMagic/VTMagic-umbrella.h
Pods/Target Support Files/VTMagic/VTMagic.debug.xcconfig
Pods/Target Support Files/VTMagic/VTMagic.modulemap
Pods/Target Support Files/VTMagic/VTMagic.release.xcconfig
Pods/Target Support Files/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework-xcframeworks-input-files.xcfilelist
Pods/Target Support Files/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework-xcframeworks-output-files.xcfilelist
Pods/Target Support Files/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework-xcframeworks.sh
Pods/Target Support Files/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.debug.xcconfig
Pods/Target Support Files/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.release.xcconfig
Pods/Target Support Files/XCGLogger/XCGLogger-Info.plist
Pods/Target Support Files/XCGLogger/XCGLogger-dummy.m
Pods/Target Support Files/XCGLogger/XCGLogger-prefix.pch
Pods/Target Support Files/XCGLogger/XCGLogger-umbrella.h
Pods/Target Support Files/XCGLogger/XCGLogger.debug.xcconfig
Pods/Target Support Files/XCGLogger/XCGLogger.modulemap
Pods/Target Support Files/XCGLogger/XCGLogger.release.xcconfig
Pods/UserDefaultsStore/LICENSE
Pods/UserDefaultsStore/README.md
Pods/UserDefaultsStore/Sources/Identifiable.swift
Pods/UserDefaultsStore/Sources/SingleUserDefaultsStore.swift
Pods/UserDefaultsStore/Sources/UserDefaultsStore.swift
Pods/VTMagic/LICENSE
Pods/VTMagic/README.md
Pods/VTMagic/VTMagic/UIColor+VTMagic.h
Pods/VTMagic/VTMagic/UIColor+VTMagic.m
Pods/VTMagic/VTMagic/UIScrollView+VTMagic.h
Pods/VTMagic/VTMagic/UIScrollView+VTMagic.m
Pods/VTMagic/VTMagic/UIViewController+VTMagic.h
Pods/VTMagic/VTMagic/UIViewController+VTMagic.m
Pods/VTMagic/VTMagic/VTContentView.h
Pods/VTMagic/VTMagic/VTContentView.m
Pods/VTMagic/VTMagic/VTEnumType.h
Pods/VTMagic/VTMagic/VTMagic.h
Pods/VTMagic/VTMagic/VTMagicController.h
Pods/VTMagic/VTMagic/VTMagicController.m
Pods/VTMagic/VTMagic/VTMagicMacros.h
Pods/VTMagic/VTMagic/VTMagicProtocol.h
Pods/VTMagic/VTMagic/VTMagicView.h
Pods/VTMagic/VTMagic/VTMagicView.m
Pods/VTMagic/VTMagic/VTMenuBar.h
Pods/VTMagic/VTMagic/VTMenuBar.m
Pods/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.xcframework/.DS_Store
Pods/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.xcframework/Info.plist
Pods/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.xcframework/README.txt
Pods/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.xcframework/ios-arm64_armv7/Headers/WXApi.h
Pods/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.xcframework/ios-arm64_armv7/Headers/WXApiObject.h
Pods/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.xcframework/ios-arm64_armv7/Headers/WechatAuthSDK.h
Pods/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.xcframework/ios-arm64_armv7/libWechatOpenSDK.a
Pods/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.xcframework/ios-arm64_i386_x86_64-simulator/Headers/WXApi.h
Pods/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.xcframework/ios-arm64_i386_x86_64-simulator/Headers/WXApiObject.h
Pods/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.xcframework/ios-arm64_i386_x86_64-simulator/Headers/WechatAuthSDK.h
Pods/WechatOpenSDK-XCFramework/WechatOpenSDK-XCFramework.xcframework/ios-arm64_i386_x86_64-simulator/libWechatOpenSDK.a
Pods/XCGLogger/.swift-version
Pods/XCGLogger/LICENSE.txt
Pods/XCGLogger/README.md
Pods/XCGLogger/Sources/XCGLogger/Destinations/AppleSystemLogDestination.swift
Pods/XCGLogger/Sources/XCGLogger/Destinations/AutoRotatingFileDestination.swift
Pods/XCGLogger/Sources/XCGLogger/Destinations/BaseDestination.swift
Pods/XCGLogger/Sources/XCGLogger/Destinations/BaseQueuedDestination.swift
Pods/XCGLogger/Sources/XCGLogger/Destinations/ConsoleDestination.swift
Pods/XCGLogger/Sources/XCGLogger/Destinations/DestinationProtocol.swift
Pods/XCGLogger/Sources/XCGLogger/Destinations/FileDestination.swift
Pods/XCGLogger/Sources/XCGLogger/Destinations/TestDestination.swift
Pods/XCGLogger/Sources/XCGLogger/Extensions/DispatchQueue+XCGAdditions.swift
Pods/XCGLogger/Sources/XCGLogger/Extensions/URL+XCGAdditions.swift
Pods/XCGLogger/Sources/XCGLogger/Filters/DevFilter.swift
Pods/XCGLogger/Sources/XCGLogger/Filters/FileNameFilter.swift
Pods/XCGLogger/Sources/XCGLogger/Filters/FilterProtocol.swift
Pods/XCGLogger/Sources/XCGLogger/Filters/TagFilter.swift
Pods/XCGLogger/Sources/XCGLogger/Filters/UserInfoFilter.swift
Pods/XCGLogger/Sources/XCGLogger/LogFormatters/ANSIColorLogFormatter.swift
Pods/XCGLogger/Sources/XCGLogger/LogFormatters/Base64LogFormatter.swift
Pods/XCGLogger/Sources/XCGLogger/LogFormatters/LogFormatterProtocol.swift
Pods/XCGLogger/Sources/XCGLogger/LogFormatters/PrePostFixLogFormatter.swift
Pods/XCGLogger/Sources/XCGLogger/LogFormatters/XcodeColorsLogFormatter.swift
Pods/XCGLogger/Sources/XCGLogger/Misc/HelperFunctions.swift
Pods/XCGLogger/Sources/XCGLogger/Misc/LogDetails.swift
Pods/XCGLogger/Sources/XCGLogger/XCGLogger.swift |