diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 098d153..d438843 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -30,7 +30,6 @@ 7A11474723FF2AA500B424AF /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474623FF2AA500B424AF /* User.swift */; }; 7A11474923FF2B2D00B424AF /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474823FF2B2D00B424AF /* Response.swift */; }; 7A11474B23FF368B00B424AF /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474A23FF368B00B424AF /* Settings.swift */; }; - 7A11474E23FFEE8800B424AF /* SVProgressHUD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A11474D23FFEE8800B424AF /* SVProgressHUD.framework */; }; 7A3F07AB24360DC800E59687 /* Dated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AA24360DC800E59687 /* Dated.swift */; }; 7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AC2436350B00E59687 /* SearchController.swift */; }; 7A3F07AF24366DF900E59687 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AE24366DF900E59687 /* Filter.swift */; }; @@ -39,6 +38,16 @@ 7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */; }; 7A530B802401803A00CBFE6E /* Vehicle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7F2401803A00CBFE6E /* Vehicle.swift */; }; 7A530B8B240181F500CBFE6E /* RxRealm in Frameworks */ = {isa = PBXBuildFile; productRef = 7A530B8A240181F500CBFE6E /* RxRealm */; }; + 7A64AE732469DFB600ABE48E /* DismissAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE6F2469DFB600ABE48E /* DismissAnimationController.swift */; }; + 7A64AE742469DFB600ABE48E /* MediaContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE702469DFB600ABE48E /* MediaContentView.swift */; }; + 7A64AE752469DFB600ABE48E /* MediaBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE712469DFB600ABE48E /* MediaBrowserViewController.swift */; }; + 7A64AE762469DFB600ABE48E /* ContentTransformers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE722469DFB600ABE48E /* ContentTransformers.swift */; }; + 7A64AE7E2469E16100ABE48E /* RadialGradientLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE782469E16100ABE48E /* RadialGradientLayer.swift */; }; + 7A64AE7F2469E16100ABE48E /* IndefiniteAnimatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE792469E16100ABE48E /* IndefiniteAnimatedView.swift */; }; + 7A64AE802469E16100ABE48E /* .gitkeep in Resources */ = {isa = PBXBuildFile; fileRef = 7A64AE7A2469E16100ABE48E /* .gitkeep */; }; + 7A64AE812469E16100ABE48E /* ProgressAnimatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE7B2469E16100ABE48E /* ProgressAnimatedView.swift */; }; + 7A64AE822469E16100ABE48E /* IHProgressHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE7C2469E16100ABE48E /* IHProgressHUD.swift */; }; + 7A64AE832469E16100ABE48E /* IHProgressHUD.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 7A64AE7D2469E16100ABE48E /* IHProgressHUD.bundle */; }; 7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD902242BF4A5009DE740 /* PlateView.swift */; }; 7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD90724329144009DE740 /* CenterTextLayer.swift */; }; 7A6DD90A24329541009DE740 /* RoadNumbers2.0.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */; }; @@ -47,7 +56,6 @@ 7A7547DD2403180A004E8406 /* SectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7547DB2403180A004E8406 /* SectionHeader.swift */; }; 7A7547DE2403180A004E8406 /* SectionHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7A7547DC2403180A004E8406 /* SectionHeader.xib */; }; 7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */; }; - 7A92D0AC240425B200EF3B77 /* ATGMediaBrowser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A92D0AB240425B100EF3B77 /* ATGMediaBrowser.framework */; }; 7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8B2435C38700258F61 /* CustomTextField.swift */; }; 7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8D2435D1A000258F61 /* CustomButton.swift */; }; 7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEFE727240455E200910EB7 /* SettingsController.swift */; }; @@ -80,6 +88,17 @@ 7A530B7924001D3300CBFE6E /* CheckController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckController.swift; sourceTree = ""; }; 7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleCell.swift; sourceTree = ""; }; 7A530B7F2401803A00CBFE6E /* Vehicle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicle.swift; sourceTree = ""; }; + 7A64AE6B2469DC6900ABE48E /* AutoCat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AutoCat.entitlements; sourceTree = ""; }; + 7A64AE6F2469DFB600ABE48E /* DismissAnimationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissAnimationController.swift; sourceTree = ""; }; + 7A64AE702469DFB600ABE48E /* MediaContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaContentView.swift; sourceTree = ""; }; + 7A64AE712469DFB600ABE48E /* MediaBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaBrowserViewController.swift; sourceTree = ""; }; + 7A64AE722469DFB600ABE48E /* ContentTransformers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentTransformers.swift; sourceTree = ""; }; + 7A64AE782469E16100ABE48E /* RadialGradientLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadialGradientLayer.swift; sourceTree = ""; }; + 7A64AE792469E16100ABE48E /* IndefiniteAnimatedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndefiniteAnimatedView.swift; sourceTree = ""; }; + 7A64AE7A2469E16100ABE48E /* .gitkeep */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .gitkeep; sourceTree = ""; }; + 7A64AE7B2469E16100ABE48E /* ProgressAnimatedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressAnimatedView.swift; sourceTree = ""; }; + 7A64AE7C2469E16100ABE48E /* IHProgressHUD.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IHProgressHUD.swift; sourceTree = ""; }; + 7A64AE7D2469E16100ABE48E /* IHProgressHUD.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = IHProgressHUD.bundle; sourceTree = ""; }; 7A6DD902242BF4A5009DE740 /* PlateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlateView.swift; sourceTree = ""; }; 7A6DD90724329144009DE740 /* CenterTextLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenterTextLayer.swift; sourceTree = ""; }; 7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = RoadNumbers2.0.otf; sourceTree = ""; }; @@ -105,7 +124,6 @@ 7A0516162414EC1200FC55AC /* Differentiator in Frameworks */, 7A11472823FEA1F400B424AF /* RealmSwift in Frameworks */, 7A11472123FEA18700B424AF /* RxCocoa in Frameworks */, - 7A11474E23FFEE8800B424AF /* SVProgressHUD.framework in Frameworks */, 7A0516182414EC1200FC55AC /* RxDataSources in Frameworks */, 7A051611241412CA00FC55AC /* SwiftDate in Frameworks */, 7A11472323FEA18700B424AF /* RxBlocking in Frameworks */, @@ -113,7 +131,6 @@ 7A11472B23FEA24D00B424AF /* Action in Frameworks */, 7A11471F23FEA18700B424AF /* RxRelay in Frameworks */, 7AF58D2F24029C5200CE01A0 /* MagazineLayout in Frameworks */, - 7A92D0AC240425B200EF3B77 /* ATGMediaBrowser.framework in Frameworks */, 7A530B78240010D900CBFE6E /* InputMask in Frameworks */, 7A11471D23FEA18700B424AF /* RxSwift in Frameworks */, 7A11472623FEA1F400B424AF /* Realm in Frameworks */, @@ -143,6 +160,7 @@ 7A1146FF23FDE7E500B424AF /* AutoCat */ = { isa = PBXGroup; children = ( + 7A64AE6B2469DC6900ABE48E /* AutoCat.entitlements */, 7A3F07A924360D9100E59687 /* Extensions */, 7A6DD90424326788009DE740 /* Fonts */, 7A6DD901242BF48D009DE740 /* Views */, @@ -177,6 +195,8 @@ 7A11472C23FECA3E00B424AF /* ThirdParty */ = { isa = PBXGroup; children = ( + 7A64AE772469E16100ABE48E /* IHProgressHUD */, + 7A64AE6E2469DFB600ABE48E /* ATGMediaBrowser */, 7A6DD90724329144009DE740 /* CenterTextLayer.swift */, ); path = ThirdParty; @@ -234,6 +254,30 @@ path = Cells; sourceTree = ""; }; + 7A64AE6E2469DFB600ABE48E /* ATGMediaBrowser */ = { + isa = PBXGroup; + children = ( + 7A64AE6F2469DFB600ABE48E /* DismissAnimationController.swift */, + 7A64AE702469DFB600ABE48E /* MediaContentView.swift */, + 7A64AE712469DFB600ABE48E /* MediaBrowserViewController.swift */, + 7A64AE722469DFB600ABE48E /* ContentTransformers.swift */, + ); + path = ATGMediaBrowser; + sourceTree = ""; + }; + 7A64AE772469E16100ABE48E /* IHProgressHUD */ = { + isa = PBXGroup; + children = ( + 7A64AE782469E16100ABE48E /* RadialGradientLayer.swift */, + 7A64AE792469E16100ABE48E /* IndefiniteAnimatedView.swift */, + 7A64AE7A2469E16100ABE48E /* .gitkeep */, + 7A64AE7B2469E16100ABE48E /* ProgressAnimatedView.swift */, + 7A64AE7C2469E16100ABE48E /* IHProgressHUD.swift */, + 7A64AE7D2469E16100ABE48E /* IHProgressHUD.bundle */, + ); + path = IHProgressHUD; + sourceTree = ""; + }; 7A6DD901242BF48D009DE740 /* Views */ = { isa = PBXGroup; children = ( @@ -263,7 +307,6 @@ 7A1146F923FDE7E500B424AF /* Sources */, 7A1146FA23FDE7E500B424AF /* Frameworks */, 7A1146FB23FDE7E500B424AF /* Resources */, - 7A11475123FFF02E00B424AF /* ShellScript */, ); buildRules = ( ); @@ -342,41 +385,22 @@ 7A7547DE2403180A004E8406 /* SectionHeader.xib in Resources */, 7A11470D23FDE7E600B424AF /* LaunchScreen.storyboard in Resources */, 7A6DD90A24329541009DE740 /* RoadNumbers2.0.otf in Resources */, + 7A64AE802469E16100ABE48E /* .gitkeep in Resources */, 7A11470A23FDE7E600B424AF /* Assets.xcassets in Resources */, 7A11470823FDE7E500B424AF /* Main.storyboard in Resources */, + 7A64AE832469E16100ABE48E /* IHProgressHUD.bundle in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 7A11475123FFF02E00B424AF /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "$(SRCROOT)/input.xcfilelist", - ); - inputPaths = ( - ); - outputFileListPaths = ( - "$(SRCROOT)/output.xcfilelist", - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n/usr/local/bin/carthage copy-frameworks\n"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ 7A1146F923FDE7E500B424AF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 7A530B802401803A00CBFE6E /* Vehicle.swift in Sources */, + 7A64AE822469E16100ABE48E /* IHProgressHUD.swift in Sources */, 7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */, 7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */, 7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */, @@ -386,9 +410,13 @@ 7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */, 7A3F07AB24360DC800E59687 /* Dated.swift in Sources */, 7A11474923FF2B2D00B424AF /* Response.swift in Sources */, + 7A64AE762469DFB600ABE48E /* ContentTransformers.swift in Sources */, 7A11471823FDEBFA00B424AF /* ReportController.swift in Sources */, + 7A64AE7E2469E16100ABE48E /* RadialGradientLayer.swift in Sources */, + 7A64AE7F2469E16100ABE48E /* IndefiniteAnimatedView.swift in Sources */, 7A11471A23FE839000B424AF /* AuthController.swift in Sources */, 7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */, + 7A64AE742469DFB600ABE48E /* MediaContentView.swift in Sources */, 7A7547DD2403180A004E8406 /* SectionHeader.swift in Sources */, 7AF58D58240309CA00CE01A0 /* VehicleTextParamCell.swift in Sources */, 7A11474723FF2AA500B424AF /* User.swift in Sources */, @@ -401,7 +429,10 @@ 7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */, 7A05161A2414FF0900FC55AC /* DateSection.swift in Sources */, 7A11474B23FF368B00B424AF /* Settings.swift in Sources */, + 7A64AE752469DFB600ABE48E /* MediaBrowserViewController.swift in Sources */, 7A3F07AF24366DF900E59687 /* Filter.swift in Sources */, + 7A64AE732469DFB600ABE48E /* DismissAnimationController.swift in Sources */, + 7A64AE812469E16100ABE48E /* ProgressAnimatedView.swift in Sources */, 7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -546,13 +577,10 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 46DTTB8X4S; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", - ); INFOPLIST_FILE = AutoCat/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -561,6 +589,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCat; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -570,13 +599,10 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 46DTTB8X4S; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", - ); INFOPLIST_FILE = AutoCat/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -585,6 +611,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCat; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 551a351..c135394 100644 --- a/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/RxSwiftCommunity/Action", "state": { "branch": null, - "revision": "cdade63f7bbe1f5e1eff7779e5858a796dc2c001", - "version": "4.0.1" + "revision": "1079e557787a3ffe88f833eb5f637dcf1396421b", + "version": "4.1.0" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/onevcat/Kingfisher", "state": { "branch": null, - "revision": "46bf251fee8ed426921c647790f08ca8ad0105a9", - "version": "5.13.1" + "revision": "349ed06467a6f8a4939bcb83db301542bc84eac9", + "version": "5.13.4" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/airbnb/MagazineLayout", "state": { "branch": null, - "revision": "bbbe1456c34c1abb527d05ff9da3ff2a54584d78", - "version": "1.5.5" + "revision": "4a91fb2fa75a3c498748466227fa115fd27bb100", + "version": "1.6.0" } }, { @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/realm/realm-cocoa", "state": { "branch": null, - "revision": "913d59c6522e3007c7cbac3a8e9775abf72c054c", - "version": "4.3.2" + "revision": "fa43b8e2909334c79f233ce472332c136ca108da", + "version": "4.4.1" } }, { @@ -78,8 +78,8 @@ "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", "state": { "branch": null, - "revision": "b3e888b4972d9bc76495dd74d30a8c7fad4b9395", - "version": "5.0.1" + "revision": "002d325b0bdee94e7882e1114af5ff4fe1e96afa", + "version": "5.1.1" } }, { diff --git a/AutoCat/AppDelegate.swift b/AutoCat/AppDelegate.swift index c280f2a..6f55e48 100644 --- a/AutoCat/AppDelegate.swift +++ b/AutoCat/AppDelegate.swift @@ -1,5 +1,4 @@ import UIKit -import SVProgressHUD import RealmSwift import os.log @@ -26,8 +25,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Realm.Configuration.defaultConfiguration = config - SVProgressHUD.setDefaultStyle(.dark) - SVProgressHUD.setDefaultMaskType(.black) + IHProgressHUD.set(defaultStyle: .dark) + IHProgressHUD.set(defaultMaskType: .black) return true } @@ -38,12 +37,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. + #if !targetEnvironment(macCatalyst) + if let shortcutItem = options.shortcutItem { if shortcutItem.type == "CheckNumberAction" { self.quickAction = .check } } + #endif + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } diff --git a/AutoCat/AutoCat.entitlements b/AutoCat/AutoCat.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/AutoCat/AutoCat.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/AutoCat/Base.lproj/Main.storyboard b/AutoCat/Base.lproj/Main.storyboard index 0fcf792..d400fb0 100644 --- a/AutoCat/Base.lproj/Main.storyboard +++ b/AutoCat/Base.lproj/Main.storyboard @@ -2,7 +2,7 @@ - + @@ -64,7 +64,7 @@ - + @@ -110,26 +110,26 @@ - + - + - + - + - @@ -194,14 +193,14 @@ - + - + - + @@ -533,21 +532,6 @@ - - - - - - - - - - - - - - - diff --git a/AutoCat/Controllers/AuthController.swift b/AutoCat/Controllers/AuthController.swift index 4e1ced9..2bd352a 100644 --- a/AutoCat/Controllers/AuthController.swift +++ b/AutoCat/Controllers/AuthController.swift @@ -1,7 +1,6 @@ import UIKit import RxSwift import RxCocoa -import SVProgressHUD import RealmSwift class AuthController: UIViewController { @@ -32,7 +31,7 @@ class AuthController: UIViewController { @IBAction func loginTapped(_ sender: UIButton) { guard let name = self.username.text, let pass = self.password.text else { return } - SVProgressHUD.show() + IHProgressHUD.show() Api.login(username: name, password: pass) .observeOn(MainScheduler.instance) .subscribe(onNext: self.goToMainScreen(user:), onError: self.displayError(error:)) @@ -42,7 +41,7 @@ class AuthController: UIViewController { @IBAction func signupTapped(_ sender: UIButton) { guard let name = self.username.text, let pass = self.password.text else { return } - SVProgressHUD.show() + IHProgressHUD.show() Api.signup(username: name, password: pass) .observeOn(MainScheduler.instance) .subscribe(onNext: self.goToMainScreen(user:), onError: self.displayError(error:)) @@ -51,11 +50,11 @@ class AuthController: UIViewController { func goToMainScreen(user: User) { guard let realm = try? Realm() else { - SVProgressHUD.showError(withStatus: "Database error") + IHProgressHUD.showError(withStatus: "Database error") return } - SVProgressHUD.dismiss() + IHProgressHUD.dismiss() if user.login != Settings.shared.user.login { try? realm.write { @@ -69,7 +68,7 @@ class AuthController: UIViewController { } func displayError(error: Error) { - SVProgressHUD.showError(withStatus: error.localizedDescription) + IHProgressHUD.showError(withStatus: error.localizedDescription) print(error) } } diff --git a/AutoCat/Controllers/CheckController.swift b/AutoCat/Controllers/CheckController.swift index 8981ef2..6e91275 100644 --- a/AutoCat/Controllers/CheckController.swift +++ b/AutoCat/Controllers/CheckController.swift @@ -2,7 +2,6 @@ import UIKit import InputMask import RealmSwift import RxSwift -import SVProgressHUD import SwiftDate import RxRealm import RxDataSources @@ -84,7 +83,7 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener { self.number.resignFirstResponder() self.number.text = nil self.check.isEnabled = false - SVProgressHUD.show() + IHProgressHUD.show() Api.checkVehicle(by: numberNormalized) .observeOn(MainScheduler.instance) .subscribe(onNext: onReceivedVehicle(_:), onError: { err in @@ -93,7 +92,7 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener { realm.add(Vehicle(numberNormalized)) } } - SVProgressHUD.showError(withStatus: err.localizedDescription) + IHProgressHUD.showError(withStatus: err.localizedDescription) print(err.localizedDescription) }).disposed(by: self.bag) } @@ -117,7 +116,7 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener { } self.updateDetailController(with: vehicle) - SVProgressHUD.dismiss() + IHProgressHUD.dismiss() } func updateDetailController(with vehicle: Vehicle) { diff --git a/AutoCat/Controllers/ReportController.swift b/AutoCat/Controllers/ReportController.swift index 4bf9365..8d563a9 100644 --- a/AutoCat/Controllers/ReportController.swift +++ b/AutoCat/Controllers/ReportController.swift @@ -1,6 +1,5 @@ import UIKit import MagazineLayout -import ATGMediaBrowser import Kingfisher enum ReportSection: Int, CaseIterable, CustomStringConvertible { @@ -309,7 +308,7 @@ class ReportController: UIViewController, UICollectionViewDataSource, UICollecti } } - func mediaBrowser(_ mediaBrowser: ATGMediaBrowser.MediaBrowserViewController, didChangeFocusTo index: Int) { + func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, didChangeFocusTo index: Int) { guard let photo = self.vehicle?.photos[index] else { return } mediaBrowser.title = photo.description } diff --git a/AutoCat/ThirdParty/ATGMediaBrowser/ContentTransformers.swift b/AutoCat/ThirdParty/ATGMediaBrowser/ContentTransformers.swift new file mode 100644 index 0000000..db63a50 --- /dev/null +++ b/AutoCat/ThirdParty/ATGMediaBrowser/ContentTransformers.swift @@ -0,0 +1,236 @@ +// +// ContentTransformers.swift +// ATGMediaBrowser +// +// Created by Suraj Thomas K on 7/17/18. +// Copyright © 2018 Al Tayer Group LLC. +// +// 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 UIKit + +/** + Content transformer used for transition between media item views. + + - parameter contentView: The content view on which transform corresponding to the position has to be applied. + - parameter position: Current position for the passed content view. + + - note: + The trasnform to be applied on the contentView has to be dependent on the position passed. + The position value can be -ve, 0.0 or positive. + + Try to visualize content views at -1.0[previous]=>0.0[current]=>1.0[next]. + + 1. When position is -1.0, the content view should be at the place meant for previous view. + + 2. When the position is 0.0, the transform applied on the content view should make it visible full screen at origin. + + 3. When position is 1.0, the content view should be at the place meant for next view. + + Be mindful of the drawing order, when designing new transitions. + */ +public typealias ContentTransformer = (_ contentView: UIView, _ position: CGFloat) -> Void + +// MARK: - Default Transitions + +/// An enumeration to hold default content transformers +public enum DefaultContentTransformers { + + /** + Horizontal move-in-out content transformer. + + - Requires: + * GestureDirection: Horizontal + */ + public static let horizontalMoveInOut: ContentTransformer = { contentView, position in + + let widthIncludingGap = contentView.bounds.size.width + MediaContentView.interItemSpacing + contentView.transform = CGAffineTransform(translationX: widthIncludingGap * position, y: 0.0) + } + + /** + Vertical move-in-out content transformer. + + - Requires: + * GestureDirection: Vertical + */ + public static let verticalMoveInOut: ContentTransformer = { contentView, position in + + let heightIncludingGap = contentView.bounds.size.height + MediaContentView.interItemSpacing + contentView.transform = CGAffineTransform(translationX: 0.0, y: heightIncludingGap * position) + } + + /** + Horizontal slide-out content transformer. + + - Requires: + * GestureDirection: Horizontal + * DrawOrder: PreviousToNext + */ + public static let horizontalSlideOut: ContentTransformer = { contentView, position in + + var scale: CGFloat = 1.0 + if position < -0.5 { + scale = 0.9 + } else if -0.5...0.0 ~= Double(position) { + scale = 1.0 + (position * 0.2) + } + var transform = CGAffineTransform(scaleX: scale, y: scale) + + let widthIncludingGap = contentView.bounds.size.width + MediaContentView.interItemSpacing + let x = position >= 0.0 ? widthIncludingGap * position : 0.0 + transform = transform.translatedBy(x: x, y: 0.0) + + contentView.transform = transform + + let margin: CGFloat = 0.0000001 + contentView.isHidden = ((1.0-margin)...(1.0+margin) ~= abs(position)) + } + + /** + Vertical slide-out content transformer. + + - Requires: + * GestureDirection: Vertical + * DrawOrder: PreviousToNext + */ + public static let verticalSlideOut: ContentTransformer = { contentView, position in + + var scale: CGFloat = 1.0 + if position < -0.5 { + scale = 0.9 + } else if -0.5...0.0 ~= Double(position) { + scale = 1.0 + (position * 0.2) + } + var transform = CGAffineTransform(scaleX: scale, y: scale) + + let heightIncludingGap = contentView.bounds.size.height + MediaContentView.interItemSpacing + let y = position >= 0.0 ? heightIncludingGap * position : 0.0 + transform = transform.translatedBy(x: 0.0, y: y) + + contentView.transform = transform + + let margin: CGFloat = 0.0000001 + contentView.isHidden = ((1.0-margin)...(1.0+margin) ~= abs(position)) + } + + /** + Horizontal slide-in content transformer. + + - Requires: + * GestureDirection: Horizontal + * DrawOrder: NextToPrevious + */ + public static let horizontalSlideIn: ContentTransformer = { contentView, position in + + var scale: CGFloat = 1.0 + if position > 0.5 { + scale = 0.9 + } else if 0.0...0.5 ~= Double(position) { + scale = 1.0 - (position * 0.2) + } + var transform = CGAffineTransform(scaleX: scale, y: scale) + + let widthIncludingGap = contentView.bounds.size.width + MediaContentView.interItemSpacing + let x = position > 0.0 ? 0.0 : widthIncludingGap * position + transform = transform.translatedBy(x: x, y: 0.0) + + contentView.transform = transform + + let margin: CGFloat = 0.0000001 + contentView.isHidden = ((1.0-margin)...(1.0+margin) ~= abs(position)) + } + + /** + Vertical slide-in content transformer. + + - Requires: + * GestureDirection: Vertical + * DrawOrder: NextToPrevious + */ + public static let verticalSlideIn: ContentTransformer = { contentView, position in + + var scale: CGFloat = 1.0 + if position > 0.5 { + scale = 0.9 + } else if 0.0...0.5 ~= Double(position) { + scale = 1.0 - (position * 0.2) + } + var transform = CGAffineTransform(scaleX: scale, y: scale) + + let heightIncludingGap = contentView.bounds.size.height + MediaContentView.interItemSpacing + let y = position > 0.0 ? 0.0 : heightIncludingGap * position + transform = transform.translatedBy(x: 0.0, y: y) + + contentView.transform = transform + + let margin: CGFloat = 0.0000001 + contentView.isHidden = ((1.0-margin)...(1.0+margin) ~= abs(position)) + } + + /** + Horizontal zoom-in-out content transformer. + + - Requires: + * GestureDirection: Horizontal + */ + public static let horizontalZoomInOut: ContentTransformer = { contentView, position in + + let minScale: CGFloat = 0.5 + // Scale factor is used to reduce the scale animation speed. + let scaleFactor: CGFloat = 0.5 + var scale: CGFloat = CGFloat.maximum(minScale, 1.0 - abs(position * scaleFactor)) + + // Actual gap will be scaleFactor * 0.5 times of contentView.bounds.size.width. + let actualGap = contentView.bounds.size.width * scaleFactor * 0.5 + let gapCorrector = MediaContentView.interItemSpacing - actualGap + + let widthIncludingGap = contentView.bounds.size.width + gapCorrector + let translation = (widthIncludingGap * position)/scale + + var transform = CGAffineTransform(scaleX: scale, y: scale) + transform = transform.translatedBy(x: translation, y: 0.0) + + contentView.transform = transform + } + + /** + Vertical zoom-in-out content transformer. + + - Requires: + * GestureDirection: Vertical + */ + public static let verticalZoomInOut: ContentTransformer = { contentView, position in + + let minScale: CGFloat = 0.5 + // Scale factor is used to reduce the scale animation speed. + let scaleFactor: CGFloat = 0.5 + let scale: CGFloat = CGFloat.maximum(minScale, 1.0 - abs(position * scaleFactor)) + + // Actual gap will be scaleFactor * 0.5 times of contentView.bounds.size.height. + let actualGap = contentView.bounds.size.height * scaleFactor * 0.5 + let gapCorrector = MediaContentView.interItemSpacing - actualGap + + let heightIncludingGap = contentView.bounds.size.height + gapCorrector + let translation = (heightIncludingGap * position)/scale + + var transform = CGAffineTransform(scaleX: scale, y: scale) + transform = transform.translatedBy(x: 0.0, y: translation) + + contentView.transform = transform + } +} diff --git a/AutoCat/ThirdParty/ATGMediaBrowser/DismissAnimationController.swift b/AutoCat/ThirdParty/ATGMediaBrowser/DismissAnimationController.swift new file mode 100644 index 0000000..0ab44fa --- /dev/null +++ b/AutoCat/ThirdParty/ATGMediaBrowser/DismissAnimationController.swift @@ -0,0 +1,288 @@ +// +// DismissAnimationController.swift +// ATGMediaBrowser +// +// Created by Suraj Thomas K on 7/19/18. +// Copyright © 2018 Al Tayer Group LLC. +// +// 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 UIKit + +internal class DismissAnimationController: NSObject { + + private enum Constants { + + static let minimumVelocity: CGFloat = 15.0 + static let minimumTranslation: CGFloat = 0.25 + static let transitionDuration = 0.3 + static let updateFrameRate: CGFloat = 60.0 + static let transitionSpeedFactor: CGFloat = 0.15 + static let minimumZoomDuringInteraction: CGFloat = 0.9 + } + + internal var image: UIImage? + internal let gestureDirection: MediaBrowserViewController.GestureDirection + internal weak var viewController: MediaBrowserViewController? + internal var interactionInProgress = false + + private lazy var imageView = UIImageView() + private var backgroundView: UIView? + + private var timer: Timer? + private var distanceToMove: CGPoint = .zero + private var relativePosition: CGPoint = .zero + private var progressValue: CGFloat { + return (gestureDirection == .horizontal) ? relativePosition.y : relativePosition.x + } + private var shouldZoomOutOnInteraction = false + + init( + image: UIImage? = nil, + gestureDirection: MediaBrowserViewController.GestureDirection, + viewController: MediaBrowserViewController + ) { + + self.image = image + self.gestureDirection = gestureDirection + self.viewController = viewController + } + + internal func handleInteractiveTransition(_ recognizer: UIPanGestureRecognizer) { + + let translation = recognizer.translation(in: recognizer.view) + + let progress = CGPoint( + x: translation.x / UIScreen.main.bounds.size.width, + y: translation.y / UIScreen.main.bounds.size.height + ) + + switch recognizer.state { + case .began: + beginTransition() + fallthrough + case .changed: + relativePosition = progress + updateTransition() + case .ended, .cancelled, .failed: + var toMove: CGFloat = 0.0 + + if abs(progressValue) > Constants.minimumTranslation { + if let viewController = viewController, + let targetFrame = viewController.dataSource?.targetFrameForDismissal(viewController) { + + animateToTargetFrame(targetFrame) + return + + } else { + toMove = (progressValue / abs(progressValue)) + } + } else { + toMove = -progressValue + } + + if gestureDirection == .horizontal { + distanceToMove.x = -relativePosition.x + distanceToMove.y = toMove + } else { + distanceToMove.x = toMove + distanceToMove.y = -relativePosition.y + } + + if timer == nil { + timer = Timer.scheduledTimer( + timeInterval: 1.0/Double(Constants.updateFrameRate), + target: self, + selector: #selector(update(_:)), + userInfo: nil, + repeats: true + ) + } + default: + break + } + } + + internal func animateToTargetFrame(_ target: CGRect) { + + let frame = imageViewFrame(for: imageView.bounds.size, in: target, mode: .scaleAspectFill) + UIView.animate(withDuration: Constants.transitionDuration, animations: { + + self.imageView.frame = frame + self.backgroundView?.alpha = 0.0 + }) { finished in + + if finished { + self.interactionInProgress = false + if self.gestureDirection == .horizontal { + self.relativePosition.y = -1.0 + } else { + self.relativePosition.x = -1.0 + } + self.finishTransition() + } + } + } + + @objc private func update(_ timeInterval: TimeInterval) { + + let speed = (Constants.updateFrameRate * Constants.transitionSpeedFactor) + let xDistance = distanceToMove.x / speed + let yDistance = distanceToMove.y / speed + distanceToMove.x -= xDistance + distanceToMove.y -= yDistance + relativePosition.x += xDistance + relativePosition.y += yDistance + updateTransition() + + let translation = CGPoint( + x: xDistance * (UIScreen.main.bounds.size.width), + y: yDistance * (UIScreen.main.bounds.size.height) + ) + let directionalTranslation = (gestureDirection == .horizontal) ? translation.y : translation.x + if abs(directionalTranslation) < 1.0 { + + relativePosition.x += distanceToMove.x + relativePosition.y += distanceToMove.y + updateTransition() + interactionInProgress = false + + finishTransition() + } + } + + internal func beginTransition() { + + shouldZoomOutOnInteraction = false + if let viewController = viewController { + shouldZoomOutOnInteraction = viewController.dataSource?.targetFrameForDismissal(viewController) != nil + } + + createTransitionViews() + + viewController?.mediaContainerView.isHidden = true + viewController?.hideControls = true + viewController?.visualEffectContainer.isHidden = true + } + + private func finishTransition() { + + distanceToMove = .zero + timer?.invalidate() + timer = nil + + imageView.removeFromSuperview() + + backgroundView?.removeFromSuperview() + backgroundView = nil + + let directionalPosition = (gestureDirection == .horizontal) ? relativePosition.y : relativePosition.x + if directionalPosition != 0.0 { + viewController?.dismiss(animated: false, completion: nil) + } else { + viewController?.mediaContainerView.isHidden = false + viewController?.hideControls = false + viewController?.visualEffectContainer.isHidden = false + } + } + + private func createTransitionViews() { + + backgroundView?.removeFromSuperview() + backgroundView = nil + + if let viewController = viewController, + let bg = viewController.visualEffectContainer.snapshotView(afterScreenUpdates: true) { + backgroundView = bg + viewController.view.addSubview(bg) + NSLayoutConstraint.activate([ + bg.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor), + bg.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor), + bg.topAnchor.constraint(equalTo: viewController.view.topAnchor), + bg.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor) + ]) + } + + imageView.image = image + imageView.frame = imageViewFrame( + for: image?.size ?? .zero, + in: viewController?.view.bounds ?? .zero + ) + viewController?.view.addSubview(imageView) + imageView.transform = CGAffineTransform.identity + } + + private func updateTransition() { + + var transform = CGAffineTransform.identity + let directionalPosition = (gestureDirection == .horizontal) ? relativePosition.y : relativePosition.x + + if shouldZoomOutOnInteraction { + let scale = CGFloat.maximum(Constants.minimumZoomDuringInteraction, 1.0 - abs(directionalPosition)) + transform = transform.scaledBy(x: scale, y: scale) + } + + if gestureDirection == .horizontal { + transform = transform.translatedBy( + x: shouldZoomOutOnInteraction ? relativePosition.x * UIScreen.main.bounds.size.width : 0.0, + y: relativePosition.y * UIScreen.main.bounds.size.height + ) + } else { + transform = transform.translatedBy( + x: relativePosition.x * UIScreen.main.bounds.size.width, + y: shouldZoomOutOnInteraction ? relativePosition.y * UIScreen.main.bounds.size.height : 0.0 + ) + } + imageView.transform = transform + + let alpha = (directionalPosition < 0.0) ? directionalPosition + 1.0 : 1.0 - directionalPosition + backgroundView?.alpha = alpha + } + + + private func imageViewFrame(for imageSize: CGSize, in frame: CGRect, mode: UIView.ContentMode = .scaleAspectFit) -> CGRect { + + guard imageSize != .zero, + mode == .scaleAspectFit || mode == .scaleAspectFill else { + return frame + } + + var targetImageSize = frame.size + + let aspectHeight = frame.size.width / imageSize.width * imageSize.height + let aspectWidth = frame.size.height / imageSize.height * imageSize.width + + if imageSize.width / imageSize.height > frame.size.width / frame.size.height { + if mode == .scaleAspectFit { + targetImageSize.height = aspectHeight + } else { + targetImageSize.width = aspectWidth + } + } else { + if mode == .scaleAspectFit { + targetImageSize.width = aspectWidth + } else { + targetImageSize.height = aspectHeight + } + } + + let x = frame.minX + (frame.size.width - targetImageSize.width) / 2.0 + let y = frame.minY + (frame.size.height - targetImageSize.height) / 2.0 + + return CGRect(origin: CGPoint(x: x, y: y), size: targetImageSize) + } +} diff --git a/AutoCat/ThirdParty/ATGMediaBrowser/MediaBrowserViewController.swift b/AutoCat/ThirdParty/ATGMediaBrowser/MediaBrowserViewController.swift new file mode 100644 index 0000000..a98014a --- /dev/null +++ b/AutoCat/ThirdParty/ATGMediaBrowser/MediaBrowserViewController.swift @@ -0,0 +1,1019 @@ +// +// MediaBrowserViewController.swift +// ATGMediaBrowser +// +// Created by Suraj Thomas K on 7/10/18. +// Copyright © 2018 Al Tayer Group LLC. +// +// 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 UIKit + +// MARK: - MediaBrowserViewControllerDataSource protocol +/// Protocol to supply media browser contents. +public protocol MediaBrowserViewControllerDataSource: class { + + /** + Completion block for passing requested media image with details. + - parameter index: Index of the requested media. + - parameter image: Image to be passed back to media browser. + - parameter zoomScale: Zoom scale to be applied to the image including min and max levels. + - parameter error: Error received while fetching the media image. + + - note: + Remember to pass the index received in the datasource method back. + This index is used to set the image to the correct image view. + */ + typealias CompletionBlock = (_ index: Int, _ image: UIImage?, _ zoomScale: ZoomScale?, _ error: Error?) -> Void + + /** + Method to supply number of items to be shown in media browser. + - parameter mediaBrowser: Reference to media browser object. + - returns: An integer with number of items to be shown in media browser. + */ + func numberOfItems(in mediaBrowser: MediaBrowserViewController) -> Int + + /** + Method to supply image for specific index. + - parameter mediaBrowser: Reference to media browser object. + - parameter index: Index of the requested media. + - parameter completion: Completion block to be executed on fetching the media image. + */ + func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, imageAt index: Int, completion: @escaping CompletionBlock) + + /** + This **optional method** callback is provided to update the styling of close button. + - parameter mediaBrowser: Reference to media browser object. + - parameter button: Reference to close button + + - note: + You can modify the styling of the supplied button, and even add constraints to position + the button relative to it's superview. Remember that if no constraints are applied on the button, + default constraints will be applied on, and will be shown on top-right side of the view. + + On top of that you can add target to this button to handle the closebutton event manually. By + default touch-up-inside event is used to close the media browser. + */ + func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, updateCloseButton button: UIButton) + + /** + This method is used to get the target frame into which the browser will perform the dismiss transition. + - parameter mediaBrowser: Reference to media browser object. + + - note: + If this method is not implemented, the media browser will perform slide up/down transition on dismissal. + */ + func targetFrameForDismissal(_ mediaBrowser: MediaBrowserViewController) -> CGRect? +} + +extension MediaBrowserViewControllerDataSource { + + public func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, updateCloseButton button: UIButton) {} + public func targetFrameForDismissal(_ mediaBrowser: MediaBrowserViewController) -> CGRect? { return nil } +} + +// MARK: - MediaBrowserViewControllerDelegate protocol + +public protocol MediaBrowserViewControllerDelegate: class { + + /** + Method invoked on scrolling to next/previous media items. + - parameter mediaBrowser: Reference to media browser object. + - parameter index: Index of the newly focussed media item. + - note: + This method will not be called on first load, and will be called only on swiping left and right. + */ + func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, didChangeFocusTo index: Int) +} + +extension MediaBrowserViewControllerDelegate { + + public func mediaBrowser(_ mediaBrowser: MediaBrowserViewController, didChangeFocusTo index: Int) {} +} + +public class MediaBrowserViewController: UIViewController { + + // MARK: - Exposed Enumerations + + /** + Enum to hold supported gesture directions. + + ``` + case horizontal + case vertical + ``` + */ + public enum GestureDirection { + + /// Horizontal (left - right) gestures. + case horizontal + /// Vertical (up - down) gestures. + case vertical + } + + /** + Enum to hold supported browser styles. + + ``` + case linear + case carousel + ``` + */ + public enum BrowserStyle { + + /// Linear browser with *0* as first index and *numItems-1* as last index. + case linear + /// Carousel browser. The media items are repeated in a circular fashion. + case carousel + } + + /** + Enum to hold supported content draw orders. + + ``` + case previousToNext + case nextToPrevious + ``` + - note: + Remember that this is draw order, not positioning. This order decides which item will + be above or below other items, when they overlap. + */ + public enum ContentDrawOrder { + + /// In this mode, media items are rendered in [previous]-[current]-[next] order. + case previousToNext + /// In this mode, media items are rendered in [next]-[current]-[previous] order. + case nextToPrevious + } + + /** + Struct to hold support for customize title style + + ``` + font + textColor + ``` + */ + public struct TitleStyle { + + /// Title style font + public var font: UIFont = UIFont.preferredFont(forTextStyle: .subheadline) + /// Title style text color. + public var textColor: UIColor = .white + } + + // MARK: - Exposed variables + + /// Data-source object to supply media browser contents. + public weak var dataSource: MediaBrowserViewControllerDataSource? + /// Delegate object to get callbacks on media browser events. + public weak var delegate: MediaBrowserViewControllerDelegate? + + /// Gesture direction. Default is `horizontal`. + public var gestureDirection: GestureDirection = .horizontal + /// Content transformer closure. Default is `horizontalMoveInOut`. + public var contentTransformer: ContentTransformer = DefaultContentTransformers.horizontalMoveInOut { + didSet { + + MediaContentView.contentTransformer = contentTransformer + contentViews.forEach({ $0.updateTransform() }) + } + } + /// Content draw order. Default is `previousToNext`. + public var drawOrder: ContentDrawOrder = .previousToNext { + didSet { + if oldValue != drawOrder { + mediaContainerView.exchangeSubview(at: 0, withSubviewAt: 2) + } + } + } + /// Browser style. Default is carousel. + public var browserStyle: BrowserStyle = .carousel + /// Gap between consecutive media items. Default is `50.0`. + public var gapBetweenMediaViews: CGFloat = Constants.gapBetweenContents { + didSet { + MediaContentView.interItemSpacing = gapBetweenMediaViews + contentViews.forEach({ $0.updateTransform() }) + } + } + /// Variable to set title style in media browser. + public var titleStyle: TitleStyle = TitleStyle() { + didSet { + configureTitleLabel() + } + } + /// Variable to set title in media browser + public override var title: String? { + didSet { + titleLabel.text = title + } + } + /// Variable to hide/show title control in media browser. Default is false. + public var shouldShowTitle: Bool = false { + didSet { + titleLabel.isHidden = !shouldShowTitle + } + } + /// Variable to hide/show page control in media browser. + public var shouldShowPageControl: Bool = true { + didSet { + pageControl.isHidden = !shouldShowPageControl + } + } + /// Variable to hide/show controls(close & page control). Default is false. + public var hideControls: Bool = false { + didSet { + hideControlViews(hideControls) + } + } + /** + Variable to schedule/cancel auto-hide controls(close & page control). Default is false. + Default delay is `3.0` seconds. + - todo: Update to accept auto-hide-delay. + */ + public var autoHideControls: Bool = false { + didSet { + if autoHideControls { + DispatchQueue.main.asyncAfter( + deadline: .now() + Constants.controlHideDelay, + execute: controlToggleTask + ) + } else { + controlToggleTask.cancel() + } + } + } + /// Enable or disable interactive dismissal. Default is enabled. + public var enableInteractiveDismissal: Bool = true + /// Item index of the current item. In range `0.. 0 { + updateContents(of: mediaView) + } + } + if drawOrder == .nextToPrevious { + mediaContainerView.exchangeSubview(at: 0, withSubviewAt: 2) + } + } + + private func addCloseButton() { + + view.addSubview(closeButton) + dataSource?.mediaBrowser(self, updateCloseButton: closeButton) + + if closeButton.constraints.isEmpty { + closeButton.translatesAutoresizingMaskIntoConstraints = false + var topAnchor = view.topAnchor + if #available(iOS 11.0, *) { + if view.responds(to: #selector(getter: UIView.safeAreaLayoutGuide)) { + topAnchor = view.safeAreaLayoutGuide.topAnchor + } + } + + NSLayoutConstraint.activate([ + closeButton.topAnchor.constraint(equalTo: topAnchor, constant: Constants.Close.top), + closeButton.trailingAnchor.constraint( + equalTo: view.trailingAnchor, + constant: Constants.Close.trailing + ), + closeButton.widthAnchor.constraint(greaterThanOrEqualToConstant: Constants.Close.minWidth), + closeButton.heightAnchor.constraint(equalToConstant: Constants.Close.height) + ]) + } + controlViews.append(closeButton) + } + + private func addPageControl() { + + view.addSubview(pageControl) + pageControl.translatesAutoresizingMaskIntoConstraints = false + var bottomAnchor = view.bottomAnchor + if #available(iOS 11.0, *) { + if view.responds(to: #selector(getter: UIView.safeAreaLayoutGuide)) { + bottomAnchor = view.safeAreaLayoutGuide.bottomAnchor + } + } + NSLayoutConstraint.activate([ + pageControl.bottomAnchor.constraint(equalTo: bottomAnchor, constant: Constants.PageControl.bottom), + pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor) + ]) + + controlViews.append(pageControl) + } + + private func addTitleLabel() { + + view.addSubview(titleLabel) + titleLabel.translatesAutoresizingMaskIntoConstraints = false + var topAnchor = view.topAnchor + if #available(iOS 11.0, *) { + if view.responds(to: #selector(getter: UIView.safeAreaLayoutGuide)) { + topAnchor = view.safeAreaLayoutGuide.topAnchor + } + } + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: Constants.Title.top), + titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor) + ]) + + controlViews.append(titleLabel) + } + + private func configureTitleLabel() { + + titleLabel.font = self.titleStyle.font + titleLabel.textColor = self.titleStyle.textColor + } + + private func hideControlViews(_ hide: Bool) { + + UIView.animate( + withDuration: Constants.animationDuration, + delay: 0.0, + options: .beginFromCurrentState, + animations: { + self.controlViews.forEach { $0.alpha = hide ? 0.0 : 1.0 } + }, + completion: nil + ) + } + + @objc private func didTapOnClose(_ sender: UIButton) { + + if let targetFrame = dataSource?.targetFrameForDismissal(self) { + dismissController.image = sourceImage() + dismissController.beginTransition() + dismissController.animateToTargetFrame(targetFrame) + } else { + dismiss(animated: true, completion: nil) + } + } +} + +// MARK: - Gesture Recognizers + +extension MediaBrowserViewController { + + @objc private func panGestureEvent(_ recognizer: UIPanGestureRecognizer) { + + if dismissController.interactionInProgress { + dismissController.handleInteractiveTransition(recognizer) + return + } + + guard numMediaItems > 0 else { + return + } + + let translation = recognizer.translation(in: view) + + switch recognizer.state { + case .began: + previousTranslation = translation + distanceToMove = 0.0 + timer?.invalidate() + timer = nil + case .changed: + moveViews(by: CGPoint(x: translation.x - previousTranslation.x, y: translation.y - previousTranslation.y)) + case .ended, .failed, .cancelled: + let velocity = recognizer.velocity(in: view) + + var viewsCopy = contentViews + let previousView = viewsCopy.removeFirst() + let middleView = viewsCopy.removeFirst() + let nextView = viewsCopy.removeFirst() + + var toMove: CGFloat = 0.0 + let directionalVelocity = gestureDirection == .horizontal ? velocity.x : velocity.y + + if abs(directionalVelocity) < Constants.minimumVelocity && + abs(middleView.position) < Constants.minimumTranslation { + toMove = -middleView.position + } else if directionalVelocity < 0.0 { + if middleView.position >= 0.0 { + toMove = -middleView.position + } else { + toMove = -nextView.position + } + } else { + if middleView.position <= 0.0 { + toMove = -middleView.position + } else { + toMove = -previousView.position + } + } + + if browserStyle == .linear || numMediaItems <= 1 { + if (middleView.index == 0 && ((middleView.position + toMove) > 0.0)) || + (middleView.index == (numMediaItems - 1) && (middleView.position + toMove) < 0.0) { + + toMove = -middleView.position + } + } + + distanceToMove = toMove + + if timer == nil { + timer = Timer.scheduledTimer( + timeInterval: 1.0/Double(Constants.updateFrameRate), + target: self, + selector: #selector(update(_:)), + userInfo: nil, + repeats: true + ) + } + default: + break + } + + previousTranslation = translation + } + + @objc private func tapGestureEvent(_ recognizer: UITapGestureRecognizer) { + + guard !dismissController.interactionInProgress else { + return + } + + if !controlToggleTask.isCancelled { + controlToggleTask.cancel() + } + hideControls = !hideControls + } +} + +// MARK: - Updating View Positions + +extension MediaBrowserViewController { + + @objc private func update(_ timeInterval: TimeInterval) { + + guard distanceToMove != 0.0 else { + + timer?.invalidate() + timer = nil + return + } + + let distance = distanceToMove / (Constants.updateFrameRate * 0.1) + distanceToMove -= distance + moveViewsNormalized(by: CGPoint(x: distance, y: distance)) + + let translation = CGPoint( + x: distance * (view.frame.size.width + gapBetweenMediaViews), + y: distance * (view.frame.size.height + gapBetweenMediaViews) + ) + let directionalTranslation = (gestureDirection == .horizontal) ? translation.x : translation.y + if abs(directionalTranslation) < 0.1 { + + moveViewsNormalized(by: CGPoint(x: distanceToMove, y: distanceToMove)) + distanceToMove = 0.0 + timer?.invalidate() + timer = nil + } + } + + private func moveViews(by translation: CGPoint) { + + let viewSizeIncludingGap = CGSize( + width: view.frame.size.width + gapBetweenMediaViews, + height: view.frame.size.height + gapBetweenMediaViews + ) + + let normalizedTranslation = calculateNormalizedTranslation( + translation: translation, + viewSize: viewSizeIncludingGap + ) + + moveViewsNormalized(by: normalizedTranslation) + } + + private func moveViewsNormalized(by normalizedTranslation: CGPoint) { + + let isGestureHorizontal = (gestureDirection == .horizontal) + + contentViews.forEach({ + $0.position += isGestureHorizontal ? normalizedTranslation.x : normalizedTranslation.y + }) + + var viewsCopy = contentViews + let previousView = viewsCopy.removeFirst() + let middleView = viewsCopy.removeFirst() + let nextView = viewsCopy.removeFirst() + + let viewSizeIncludingGap = CGSize( + width: view.frame.size.width + gapBetweenMediaViews, + height: view.frame.size.height + gapBetweenMediaViews + ) + + let viewSize = isGestureHorizontal ? viewSizeIncludingGap.width : viewSizeIncludingGap.height + let normalizedGap = gapBetweenMediaViews/viewSize + let normalizedCenter = (middleView.frame.size.width / viewSize) * 0.5 + let viewCount = contentViews.count + + if middleView.position < -(normalizedGap + normalizedCenter) { + + index = sanitizeIndex(index + 1) + + // Previous item is taken and placed on right/down most side + previousView.position += CGFloat(viewCount) + previousView.index += viewCount + updateContents(of: previousView) + + if let image = nextView.image { + self.visualEffectContentView.image = image + } + + contentViews.removeFirst() + contentViews.append(previousView) + + switch drawOrder { + case .previousToNext: + mediaContainerView.bringSubviewToFront(previousView) + case .nextToPrevious: + mediaContainerView.sendSubviewToBack(previousView) + } + + delegate?.mediaBrowser(self, didChangeFocusTo: index) + + } else if middleView.position > (1 + normalizedGap - normalizedCenter) { + + index = sanitizeIndex(index - 1) + + // Next item is taken and placed on left/top most side + nextView.position -= CGFloat(viewCount) + nextView.index -= viewCount + updateContents(of: nextView) + + if let image = previousView.image { + self.visualEffectContentView.image = image + } + + contentViews.removeLast() + contentViews.insert(nextView, at: 0) + + switch drawOrder { + case .previousToNext: + mediaContainerView.sendSubviewToBack(nextView) + case .nextToPrevious: + mediaContainerView.bringSubviewToFront(nextView) + } + + delegate?.mediaBrowser(self, didChangeFocusTo: index) + } + } + + private func calculateNormalizedTranslation(translation: CGPoint, viewSize: CGSize) -> CGPoint { + + guard let middleView = mediaView(at: 1) else { + return .zero + } + + var normalizedTranslation = CGPoint( + x: (translation.x)/viewSize.width, + y: (translation.y)/viewSize.height + ) + + if browserStyle != .carousel || numMediaItems <= 1 { + let isGestureHorizontal = (gestureDirection == .horizontal) + let directionalTranslation = isGestureHorizontal ? normalizedTranslation.x : normalizedTranslation.y + if (middleView.index == 0 && ((middleView.position + directionalTranslation) > 0.0)) || + (middleView.index == (numMediaItems - 1) && (middleView.position + directionalTranslation) < 0.0) { + if isGestureHorizontal { + normalizedTranslation.x *= Constants.bounceFactor + } else { + normalizedTranslation.y *= Constants.bounceFactor + } + } + } + return normalizedTranslation + } + + private func updateContents(of contentView: MediaContentView) { + + contentView.image = nil + let convertedIndex = sanitizeIndex(contentView.index) + contentView.isLoading = true + dataSource?.mediaBrowser( + self, + imageAt: convertedIndex, + completion: { [weak self] (index, image, zoom, _) in + + guard let strongSelf = self else { + return + } + + if index == strongSelf.sanitizeIndex(contentView.index) { + if image != nil { + contentView.image = image + contentView.zoomLevels = zoom + + if index == strongSelf.index { + strongSelf.visualEffectContentView.image = image + } + } + contentView.isLoading = false + } + } + ) + } + + private func sanitizeIndex(_ index: Int) -> Int { + + let newIndex = index % numMediaItems + if newIndex < 0 { + return newIndex + numMediaItems + } + return newIndex + } + + private func sourceImage() -> UIImage? { + + return mediaView(at: 1)?.image + } + + private func mediaView(at index: Int) -> MediaContentView? { + + guard index < contentViews.count else { + + assertionFailure("Content views does not have this many views. : \(index)") + return nil + } + return contentViews[index] + } +} + +// MARK: - UIGestureRecognizerDelegate + +extension MediaBrowserViewController: UIGestureRecognizerDelegate { + + public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + + guard enableInteractiveDismissal else { + return true + } + + let middleView = mediaView(at: 1) + if middleView?.zoomScale == middleView?.zoomLevels?.minimumZoomScale, + let recognizer = gestureRecognizer as? UIPanGestureRecognizer { + + let translation = recognizer.translation(in: recognizer.view) + + if gestureDirection == .horizontal { + dismissController.interactionInProgress = abs(translation.y) > abs(translation.x) + } else { + dismissController.interactionInProgress = abs(translation.x) > abs(translation.y) + } + if dismissController.interactionInProgress { + dismissController.image = sourceImage() + } + } + return true + } + + public func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + + if gestureRecognizer is UIPanGestureRecognizer, + let scrollView = otherGestureRecognizer.view as? MediaContentView { + return scrollView.zoomScale == 1.0 + } + return false + } + + public func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + + if gestureRecognizer is UITapGestureRecognizer { + return otherGestureRecognizer.view is MediaContentView + } + return false + } +} diff --git a/AutoCat/ThirdParty/ATGMediaBrowser/MediaContentView.swift b/AutoCat/ThirdParty/ATGMediaBrowser/MediaContentView.swift new file mode 100644 index 0000000..032dd94 --- /dev/null +++ b/AutoCat/ThirdParty/ATGMediaBrowser/MediaContentView.swift @@ -0,0 +1,315 @@ +// +// MediaContentView.swift +// ATGMediaBrowser +// +// Created by Suraj Thomas K on 7/10/18. +// Copyright © 2018 Al Tayer Group LLC. +// +// 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 UIKit + +/// Holds the value of minimumZoomScale and maximumZoomScale of the image. +public struct ZoomScale { + + /// Minimum zoom level, the image can be zoomed out to. + public var minimumZoomScale: CGFloat + + /// Maximum zoom level, the image can be zoomed into. + public var maximumZoomScale: CGFloat + + /// Default zoom scale. minimum is 1.0 and maximum is 3.0 + public static let `default` = ZoomScale( + minimum: 1.0, + maximum: 3.0 + ) + + /// Identity zoom scale. Pass this to disable zoom. + public static let identity = ZoomScale( + minimum: 1.0, + maximum: 1.0 + ) + + /** + Initializer. + - parameter minimum: The minimum zoom level. + - parameter maximum: The maximum zoom level. + */ + public init(minimum: CGFloat, maximum: CGFloat) { + + minimumZoomScale = minimum + maximumZoomScale = maximum + } +} + +internal class MediaContentView: UIScrollView { + + // MARK: - Exposed variables + internal static var interItemSpacing: CGFloat = 0.0 + internal var index: Int { + didSet { + resetZoom() + } + } + internal static var contentTransformer: ContentTransformer = DefaultContentTransformers.horizontalMoveInOut + + internal var position: CGFloat { + didSet { + updateTransform() + } + } + internal var image: UIImage? { + didSet { + updateImageView() + } + } + internal var isLoading: Bool = false { + didSet { + indicatorContainer.isHidden = !isLoading + if isLoading { + indicator.startAnimating() + } else { + indicator.stopAnimating() + } + } + } + internal var zoomLevels: ZoomScale? { + didSet { + zoomScale = ZoomScale.default.minimumZoomScale + minimumZoomScale = zoomLevels?.minimumZoomScale ?? ZoomScale.default.minimumZoomScale + maximumZoomScale = zoomLevels?.maximumZoomScale ?? ZoomScale.default.maximumZoomScale + } + } + + // MARK: - Private enumerations + + private enum Constants { + + static let indicatorViewSize: CGFloat = 60.0 + } + + // MARK: - Private variables + + private lazy var imageView: UIImageView = { + let imageView = UIImageView() + imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + imageView.contentMode = .scaleAspectFit + imageView.clipsToBounds = true + return imageView + }() + + private lazy var indicator: UIActivityIndicatorView = { + let indicatorView = UIActivityIndicatorView() + indicatorView.style = UIActivityIndicatorView.Style.large + indicatorView.hidesWhenStopped = true + return indicatorView + }() + + private lazy var indicatorContainer: UIView = { + let container = UIView() + container.backgroundColor = .darkGray + container.layer.cornerRadius = Constants.indicatorViewSize * 0.5 + container.layer.masksToBounds = true + return container + }() + + private lazy var doubleTapGestureRecognizer: UITapGestureRecognizer = { [unowned self] in + let gesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTap(_:))) + gesture.numberOfTapsRequired = 2 + gesture.numberOfTouchesRequired = 1 + return gesture + }() + + init(index itemIndex: Int, position: CGFloat, frame: CGRect) { + + self.index = itemIndex + self.position = position + + super.init(frame: frame) + + initializeViewComponents() + } + + required init?(coder aDecoder: NSCoder) { + + fatalError("Do nto use `init?(coder:)`") + } +} + +// MARK: - View Composition and Events + +extension MediaContentView { + + private func initializeViewComponents() { + + addSubview(imageView) + imageView.frame = frame + + setupIndicatorView() + + configureScrollView() + + addGestureRecognizer(doubleTapGestureRecognizer) + + updateTransform() + } + + private func configureScrollView() { + + isMultipleTouchEnabled = true + showsHorizontalScrollIndicator = false + showsVerticalScrollIndicator = false + contentSize = imageView.bounds.size + canCancelContentTouches = false + zoomLevels = ZoomScale.default + delegate = self + bouncesZoom = false + } + + private func resetZoom() { + + setZoomScale(1.0, animated: false) + imageView.transform = CGAffineTransform.identity + contentSize = imageView.frame.size + contentOffset = .zero + } + + private func setupIndicatorView() { + + addSubview(indicatorContainer) + indicatorContainer.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + indicatorContainer.widthAnchor.constraint(equalToConstant: Constants.indicatorViewSize), + indicatorContainer.heightAnchor.constraint(equalToConstant: Constants.indicatorViewSize), + indicatorContainer.centerXAnchor.constraint(equalTo: centerXAnchor), + indicatorContainer.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + + indicatorContainer.addSubview(indicator) + indicator.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + indicator.leadingAnchor.constraint(equalTo: indicatorContainer.leadingAnchor), + indicator.trailingAnchor.constraint(equalTo: indicatorContainer.trailingAnchor), + indicator.topAnchor.constraint(equalTo: indicatorContainer.topAnchor), + indicator.bottomAnchor.constraint(equalTo: indicatorContainer.bottomAnchor) + ]) + + indicatorContainer.setNeedsLayout() + indicatorContainer.layoutIfNeeded() + + indicatorContainer.isHidden = true + } + + internal func updateTransform() { + + MediaContentView.contentTransformer(self, position) + } + + internal func handleChangeInViewSize(to size: CGSize) { + + let oldScale = zoomScale + zoomScale = 1.0 + imageView.frame = CGRect(origin: .zero, size: size) + + updateImageView() + updateTransform() + setZoomScale(oldScale, animated: false) + + contentSize = imageView.frame.size + } + + @objc private func didDoubleTap(_ recognizer: UITapGestureRecognizer) { + + let locationInImage = recognizer.location(in: imageView) + + let isImageCoveringScreen = imageView.frame.size.width > bounds.size.width && + imageView.frame.size.height > bounds.size.height + let zoomTo = (isImageCoveringScreen || zoomScale == maximumZoomScale) ? minimumZoomScale : maximumZoomScale + + guard zoomTo != zoomScale else { + return + } + + let width = bounds.size.width / zoomTo + let height = bounds.size.height / zoomTo + + let zoomRect = CGRect( + x: locationInImage.x - width * 0.5, + y: locationInImage.y - height * 0.5, + width: width, + height: height + ) + + zoom(to: zoomRect, animated: true) + } +} + +// MARK: - UIScrollViewDelegate + +extension MediaContentView: UIScrollViewDelegate { + + internal func viewForZooming(in scrollView: UIScrollView) -> UIView? { + + let shouldAllowZoom = (image != nil && position == 0.0) + return shouldAllowZoom ? imageView : nil + } + + internal func scrollViewDidZoom(_ scrollView: UIScrollView) { + + centerImageView() + } + + private func centerImageView() { + + var imageViewFrame = imageView.frame + + if imageViewFrame.size.width < bounds.size.width { + imageViewFrame.origin.x = (bounds.size.width - imageViewFrame.size.width) / 2.0 + } else { + imageViewFrame.origin.x = 0.0 + } + + if imageViewFrame.size.height < bounds.size.height { + imageViewFrame.origin.y = (bounds.size.height - imageViewFrame.size.height) / 2.0 + } else { + imageViewFrame.origin.y = 0.0 + } + + imageView.frame = imageViewFrame + } + + private func updateImageView() { + + imageView.image = image + + if let contentImage = image { + + let imageViewSize = bounds.size + let imageSize = contentImage.size + var targetImageSize = imageViewSize + + if imageSize.width / imageSize.height > imageViewSize.width / imageViewSize.height { + targetImageSize.height = imageViewSize.width / imageSize.width * imageSize.height + } else { + targetImageSize.width = imageViewSize.height / imageSize.height * imageSize.width + } + + imageView.frame = CGRect(origin: .zero, size: targetImageSize) + } + centerImageView() + } +} diff --git a/AutoCat/ThirdParty/IHProgressHUD/.gitkeep b/AutoCat/ThirdParty/IHProgressHUD/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/angle-mask@1x.png b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/angle-mask@1x.png new file mode 100644 index 0000000..ffecbd4 Binary files /dev/null and b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/angle-mask@1x.png differ diff --git a/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/angle-mask@2x.png b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/angle-mask@2x.png new file mode 100644 index 0000000..452458e Binary files /dev/null and b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/angle-mask@2x.png differ diff --git a/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/angle-mask@3x.png b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/angle-mask@3x.png new file mode 100644 index 0000000..bf8e5e4 Binary files /dev/null and b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/angle-mask@3x.png differ diff --git a/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/error@1x.png b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/error@1x.png new file mode 100644 index 0000000..99b3694 Binary files /dev/null and b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/error@1x.png differ diff --git a/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/error@2x.png b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/error@2x.png new file mode 100644 index 0000000..5c11ed0 Binary files /dev/null and b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/error@2x.png differ diff --git a/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/error@3x.png b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/error@3x.png new file mode 100644 index 0000000..91add09 Binary files /dev/null and b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/error@3x.png differ diff --git a/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/info@1x.png b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/info@1x.png new file mode 100644 index 0000000..ebf14c4 Binary files /dev/null and b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/info@1x.png differ diff --git a/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/info@2x.png b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/info@2x.png new file mode 100644 index 0000000..d81cfd8 Binary files /dev/null and b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/info@2x.png differ diff --git a/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/info@3x.png b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/info@3x.png new file mode 100644 index 0000000..6539bda Binary files /dev/null and b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/info@3x.png differ diff --git a/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/success@1x.png b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/success@1x.png new file mode 100644 index 0000000..03338a3 Binary files /dev/null and b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/success@1x.png differ diff --git a/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/success@2x.png b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/success@2x.png new file mode 100644 index 0000000..b70ab2b Binary files /dev/null and b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/success@2x.png differ diff --git a/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/success@3x.png b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/success@3x.png new file mode 100644 index 0000000..94d545b Binary files /dev/null and b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/success@3x.png differ diff --git a/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.swift b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.swift new file mode 100755 index 0000000..3d4d66c --- /dev/null +++ b/AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.swift @@ -0,0 +1,1312 @@ +// +// Converted to Swift 4 by Swiftify v4.2.29618 - https://objectivec2swift.com/ +// +// IndefiniteAnimatedView.swift +// SVProgressHUD, https://github.com/IHProgressHUD/IHProgressHUD +// +// Original Copyright (c) 2014-2018 Guillaume Campagna. All rights reserved. +// Modified Copyright © 2018 Ibrahim Hassan. All rights reserved. +// + + +import UIKit + +public enum NotificationName : String { + case IHProgressHUDDidReceiveTouchEvent, IHProgressHUDDidTouchDownInside, IHProgressHUDWillDisappear, IHProgressHUDDidDisappear, IHProgressHUDWillAppear, IHProgressHUDDidAppear, IHProgressHUDStatusUserInfoKey + public func getNotificationName() -> Notification.Name { + return Notification.Name.init(self.rawValue) + } +} + +public enum IHProgressHUDStyle : Int { + case light + case dark + case custom +} + +public enum IHProgressHUDMaskType : Int { + case none = 1 + case clear + case black + case gradient + case custom +} + +public enum IHProgressHUDAnimationType : Int { + case flat + case native +} + +private let IHProgressHUDParallaxDepthPoints : CGFloat = 10.0 +private let IHProgressHUDUndefinedProgress : CGFloat = -1 +private let IHProgressHUDDefaultAnimationDuration: CGFloat = 0.15 +private let IHProgressHUDVerticalSpacing: CGFloat = 12.0 +private let IHProgressHUDHorizontalSpacing: CGFloat = 12.0 +private let IHProgressHUDLabelSpacing: CGFloat = 8.0 + +public class IHProgressHUD : UIView { + + static var isNotAppExtension = true + + private var defaultStyle = IHProgressHUDStyle.light + private var defaultMaskType = IHProgressHUDMaskType.none + private var defaultAnimationType = IHProgressHUDAnimationType.flat + private var containerView: UIView? + private var minimumSize = CGSize.init(width: 50, height: 50) + private var ringThickness: CGFloat = 2.0 + private var ringRadius: CGFloat = 18.0 + private var ringNoTextRadius: CGFloat = 24.0 + private var cornerRadius: CGFloat = 14.0 + private var font = UIFont.preferredFont(forTextStyle: .subheadline) + private var foregroundColor : UIColor? + private var backgroundLayerColor = UIColor.init(white: 0, alpha: 0.4) + private var imageViewSize: CGSize = CGSize.init(width: 28, height: 28) + private var shouldTintImages : Bool = true + private var infoImage: UIImage! + private var successImage: UIImage! //= UIImage.init(named: "success")! + private var errorImage: UIImage! //= UIImage.init(named: "error")! + private var viewForExtension: UIView? + private var graceTimeInterval: TimeInterval = 0.0 + private var minimumDismissTimeInterval: TimeInterval = 5.0 + private var maximumDismissTimeInterval: TimeInterval = TimeInterval(CGFloat.infinity) + private var offsetFromCenter: UIOffset = UIOffset.init(horizontal: 0, vertical: 0) + private var fadeInAnimationDuration: TimeInterval = TimeInterval(IHProgressHUDDefaultAnimationDuration) + private var fadeOutAnimationDuration: TimeInterval = TimeInterval(IHProgressHUDDefaultAnimationDuration) + private var maxSupportedWindowLevel: UIWindow.Level = UIWindow.Level.normal + private var hapticsEnabled = false + private var graceTimer: Timer? + private var fadeOutTimer: Timer? + private var controlView: UIControl? + private var backgroundView: UIView? + private var backgroundRadialGradientLayer: RadialGradientLayer? + private var hudView: UIVisualEffectView? + private var statusLabel: UILabel? + private var imageView: UIImageView? + private var indefiniteAnimatedView: IndefiniteAnimatedView? + private var ringView: ProgressAnimatedView? + private var backgroundRingView: ProgressAnimatedView? + private var progress: Float = 0.0 + private var activityCount: Int = 0 + private var visibleKeyboardHeight: CGFloat = 0.0 + private var frontWindow: UIWindow? + private var hudBackgroundColor : UIColor? + #if os(iOS) + @available(iOS 10.0, *) + private var hapticGenerator: UINotificationFeedbackGenerator? { + get { + if hapticsEnabled == true { + return UINotificationFeedbackGenerator() + } else { + return nil + } + } + } + #endif + private override init(frame: CGRect) { + super.init(frame: frame) + infoImage = loadImageBundle(named: "info")! + successImage = loadImageBundle(named: "success")! + errorImage = loadImageBundle(named: "error") + isUserInteractionEnabled = false + activityCount = 0 + getBackGroundView().alpha = 0.0 + getImageView().alpha = 0.0 + getStatusLabel().alpha = 1.0 + getIndefiniteAnimatedView().alpha = 0.0 + getBackgroundRingView().alpha = 0.0 + backgroundColor = UIColor.clear + accessibilityIdentifier = "IHProgressHUD" + isAccessibilityElement = true + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + private func getIndefiniteAnimatedView() -> IndefiniteAnimatedView { + if defaultAnimationType == .flat { + if (indefiniteAnimatedView == nil) { + indefiniteAnimatedView = IndefiniteAnimatedView.init(frame: .zero) + } + indefiniteAnimatedView?.setIndefinite(strokeColor: foreGroundColorForStyle()) + indefiniteAnimatedView?.setIndefinite(strokeThickness: ringThickness) + var radius :CGFloat = 0.0 + if getStatusLabel().text != nil { + radius = ringRadius + } else { + radius = ringNoTextRadius + } + indefiniteAnimatedView?.setIndefinite(radius: radius) + } else { + indefiniteAnimatedView?.removeAnimationLayer() + indefiniteAnimatedView?.setActivityIndicator(color: foreGroundColorForStyle()) + indefiniteAnimatedView?.startAnimation() + } + indefiniteAnimatedView?.sizeToFit() + return indefiniteAnimatedView! + } + + private static let sharedView : IHProgressHUD = { + var localInstance : IHProgressHUD? + if Thread.current.isMainThread { + if IHProgressHUD.isNotAppExtension { + if let window = UIApplication.shared.delegate?.window { + localInstance = IHProgressHUD.init(frame: window?.bounds ?? CGRect.zero) + } else { + localInstance = IHProgressHUD() + } + } + else { + localInstance = IHProgressHUD.init(frame: UIScreen.main.bounds) + } + } else { + DispatchQueue.main.sync { + if IHProgressHUD.isNotAppExtension { + if let window = UIApplication.shared.delegate?.window { + localInstance = IHProgressHUD.init(frame: window?.bounds ?? CGRect.zero) + } else { + localInstance = IHProgressHUD() + } + } else { + localInstance = IHProgressHUD.init(frame: UIScreen.main.bounds) + } + } + } + return localInstance! + }() + + // MARK :- Setters + + private func showProgress(progress: Float, status: String?) { + OperationQueue.main.addOperation({ [weak self] in + guard let strongSelf = self else { return } + if strongSelf.fadeOutTimer != nil { + strongSelf.activityCount = 0 + } + + // Stop timer + strongSelf.setFadeOut(timer: nil) + strongSelf.setGrace(timer: nil) + + // Update / Check view hierarchy to ensure the HUD is visible + strongSelf.updateViewHierarchy() + + // Reset imageView and fadeout timer if an image is currently displayed + strongSelf.getImageView().isHidden = true + strongSelf.getImageView().image = nil + + // Update text and set progress to the given value + strongSelf.getStatusLabel().isHidden = (status?.count ?? 0) == 0 + strongSelf.getStatusLabel().text = status + strongSelf.progress = progress + + // Choose the "right" indicator depending on the progress + if progress >= 0 { + // Cancel the indefiniteAnimatedView, then show the ringLayer + strongSelf.cancelIndefiniteAnimatedViewAnimation() + + // Add ring to HUD + if strongSelf.getRingView().superview == nil { + strongSelf.getHudView().contentView.addSubview(strongSelf.getRingView()) + } + if strongSelf.getBackgroundRingView().superview == nil { + strongSelf.getHudView().contentView.addSubview(strongSelf.getBackgroundRingView()) + } + + // Set progress animated + CATransaction.begin() + CATransaction.setDisableActions(true) + strongSelf.getRingView().set(strokeEnd: CGFloat(progress)) + // strongSelf.ringView.strokeEnd = progress; + CATransaction.commit() + + // Update the activity count + if progress == 0 { + strongSelf.activityCount += 1 + } + } else { + // Cancel the ringLayer animation, then show the indefiniteAnimatedView + strongSelf.cancelRingLayerAnimation() + + // Add indefiniteAnimatedView to HUD + strongSelf.getHudView().contentView.addSubview(strongSelf.getIndefiniteAnimatedView()) + + if strongSelf.defaultAnimationType == .native { + strongSelf.getIndefiniteAnimatedView().stopActivityIndicator() + } + + // Update the activity count + strongSelf.activityCount += 1 + } + + // Fade in delayed if a grace time is set + if strongSelf.graceTimeInterval > 0.0 && strongSelf.getBackGroundView().alpha == 0.0 { + let timer = Timer(timeInterval: strongSelf.graceTimeInterval, target: strongSelf, selector: #selector(strongSelf.fadeIn(_:)), userInfo: nil, repeats: false) + strongSelf.setGrace(timer: timer) + if let aTimer = strongSelf.graceTimer { + RunLoop.main.add(aTimer, forMode: .common) + } + } else { + strongSelf.fadeIn(nil) + } + + // Tell the Haptics Generator to prepare for feedback, which may come soon + #if os(iOS) + if #available(iOS 10.0, *) { + strongSelf.hapticGenerator?.prepare() + } + #endif + }) + } + + @objc private func controlViewDidReceiveTouchEvent(_ sender: Any?, for event: UIEvent?) { + NotificationCenter.default.post(name: NotificationName.IHProgressHUDDidReceiveTouchEvent.getNotificationName(), object: self, userInfo: notificationUserInfo()) + + if let touchLocation = event?.allTouches?.first?.location(in: self) { + if getHudView().frame.contains(touchLocation) { + NotificationCenter.default.post(name: + NotificationName.IHProgressHUDDidTouchDownInside.getNotificationName(), object: self, userInfo: notificationUserInfo()) + } + } + + } + + func notificationUserInfo() -> [String : Any]? { + if let statusText = getStatusLabel().text { + return [NotificationName.IHProgressHUDStatusUserInfoKey.rawValue: statusText] + } + return nil + } + + + @objc private func fadeIn(_ object: AnyObject?) { + updateHUDFrame() + positionHUD() + if (defaultMaskType != .none) { + getControlView().isUserInteractionEnabled = true + accessibilityLabel = getStatusLabel().text ?? "Loading" + isAccessibilityElement = true + getControlView().accessibilityViewIsModal = true + } else { + getControlView().isUserInteractionEnabled = false + getHudView().accessibilityLabel = getStatusLabel().text ?? "Loading" + getHudView().isAccessibilityElement = true + getControlView().accessibilityViewIsModal = false + } + + if getBackGroundView().alpha != 1.0 { + NotificationCenter.default.post(name: NotificationName.IHProgressHUDWillAppear.getNotificationName(), object: self, userInfo: notificationUserInfo()) + + getHudView().transform = CGAffineTransform.init(scaleX: 1/1.5, y: 1/1.5) + let animationsBlock : () -> Void = { + // Zoom HUD a little to make a nice appear / pop up animation + self.getHudView().transform = CGAffineTransform.identity + + // Fade in all effects (colors, blur, etc.) + self.fadeInEffects() + } + + + let completionBlock : () -> Void = { + if self.getBackGroundView().alpha == 1.0 { + self.registerNotifications() + } + + NotificationCenter.default.post(name: NotificationName.IHProgressHUDDidAppear.getNotificationName(), object: self, userInfo: self.notificationUserInfo()) + + // Update accessibility + + UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: nil) + + UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: self.statusLabel?.text) + if let cd : TimeInterval = object as? TimeInterval { + let timer = Timer.init(timeInterval: cd, target: self, selector: #selector(self.dismiss), userInfo: nil, repeats: false) + self.setFadeOut(timer: timer) + RunLoop.main.add(self.fadeOutTimer!, forMode: .common) + } + } + + if fadeInAnimationDuration > 0 { + UIView.animate(withDuration: self.fadeInAnimationDuration, delay: 0, options: [.allowUserInteraction, .curveEaseIn, .beginFromCurrentState], animations: animationsBlock, completion: { + finished in + completionBlock() + }) + } else { + animationsBlock() + completionBlock() + } + self.setNeedsDisplay() + } else { + UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: nil) + + UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: self.statusLabel?.text) + + if let convertedDuration : TimeInterval = object as? TimeInterval { + let timer = Timer.init(timeInterval: convertedDuration, target: self, selector: #selector(dismiss), userInfo: nil, repeats: false) + setFadeOut(timer: timer) + RunLoop.main.add(self.fadeOutTimer!, forMode: .common) + } + } + } + + @objc private func positionHUD(_ notification: Notification? = nil) { + var keyboardHeight: CGFloat = 0.0 + var animationDuration: Double = 0.0 + + if IHProgressHUD.isNotAppExtension == false { + if viewForExtension != nil { + frame = viewForExtension!.frame + } else { + frame = UIScreen.main.bounds + } + } + + var statusBarFrame = CGRect.zero + + #if os(iOS) // notAppExtension + iOS + var orientation = UIInterfaceOrientation.portrait + if IHProgressHUD.isNotAppExtension { + if #available(iOS 13.0, *) { + var rootVC:UIViewController? = nil + for scene in UIApplication.shared.connectedScenes { + if scene.activationState == .foregroundActive { + if let vc = ((scene as? UIWindowScene)?.delegate as? UIWindowSceneDelegate)?.window??.rootViewController { + rootVC = vc + break + } + } + } + frame = rootVC?.view.window?.bounds ?? UIScreen.main.bounds + if let or = rootVC?.view.window?.windowScene?.interfaceOrientation { + orientation = or + } + if let statFrame = rootVC?.view.window?.windowScene?.statusBarManager?.statusBarFrame { + statusBarFrame = statFrame + } + } else { + // Fallback on earlier versions + if let appDelegate = UIApplication.shared.delegate { + if let window = appDelegate.window { + if let windowFrame = window?.bounds { + frame = windowFrame + } + } + } + orientation = UIApplication.shared.statusBarOrientation + statusBarFrame = UIApplication.shared.statusBarFrame + } + + + if frame.width > frame.height { + orientation = .landscapeLeft + } else { + orientation = .portrait + } + if let notificationData = notification { + let keyboardInfo = notificationData.userInfo + if let keyboardFrame: NSValue = keyboardInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { + let keyboardFrame: CGRect = keyboardFrame.cgRectValue + if (notification?.name.rawValue == UIResponder.keyboardWillShowNotification.rawValue || notification?.name.rawValue == UIResponder.keyboardDidShowNotification.rawValue) { + keyboardHeight = keyboardFrame.width + if orientation.isPortrait { + keyboardHeight = keyboardFrame.height + } + } + } + if let aDuration: Double = keyboardInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double { + animationDuration = aDuration + } + } else { + keyboardHeight = getVisibleKeyboardHeight() + } + + updateMotionEffectForOrientation(orientation) + } + #endif + + let orientationFrame = bounds + #if os(tvOS) + if IHProgressHUD.isNotAppExtension { + if let keyWindow : UIWindow = UIApplication.shared.keyWindow { + frame = keyWindow.bounds + } + } + updateMotionEffect(forXMotionEffectType: .tiltAlongHorizontalAxis, yMotionEffectType: .tiltAlongHorizontalAxis) + #endif + + var activeHeight = orientationFrame.height + + if keyboardHeight > 0 { + activeHeight += statusBarFrame.height * 2 + } + activeHeight -= keyboardHeight + + let posX = orientationFrame.midX + let posY = CGFloat(floor(activeHeight * 0.45)) + + let rotateAngle : CGFloat = 0.0 + let newCenter = CGPoint.init(x: posX, y: posY) + + if notification != nil { + // Animate update if notification was present + UIView.animate(withDuration: TimeInterval(animationDuration), delay: 0, options: [.allowUserInteraction, .beginFromCurrentState], animations: { + self.move(to: newCenter, rotateAngle: rotateAngle) + self.getHudView().setNeedsDisplay() + }) + } else { + move(to: newCenter, rotateAngle: rotateAngle) + } + } + + private func updateViewHierarchy() { + // Add the overlay to the application window if necessary + if getControlView().superview == nil { + if containerView != nil { + self.containerView!.addSubview(getControlView()) + // self.frame = containerView!.frame + } else { + if IHProgressHUD.isNotAppExtension { + if self.containerView != nil { + containerView?.addSubview(getControlView()) + } else { + getFrontWindow()?.addSubview(getControlView()) + } + } + else { + // If IHProgressHUD is used inside an app extension add it to the given view + if viewForExtension != nil { + viewForExtension!.addSubview(getControlView()) + } + } + } + } else { + // The HUD is already on screen, but maybe not in front. Therefore + // ensure that overlay will be on top of rootViewController (which may + // be changed during runtime). + getControlView().superview?.bringSubviewToFront(getControlView()) + } + + // Add self to the overlay view + if superview == nil { + getControlView().addSubview(self) + } + } + + private func cancelIndefiniteAnimatedViewAnimation(){ + self.indefiniteAnimatedView?.stopActivityIndicator() + self.indefiniteAnimatedView?.removeFromSuperview() + } + + private func cancelRingLayerAnimation() { + // Animate value update, stop animation + CATransaction.begin() + CATransaction.setDisableActions(true) + + getHudView().layer.removeAllAnimations() + getRingView().set(strokeEnd: 0.0) + + CATransaction.commit() + + // Remove from view + getRingView().removeFromSuperview() + getBackgroundRingView().removeFromSuperview() + } + + // stops the activity indicator, shows a glyph + status, and dismisses the HUD a little bit later + + private func show(image: UIImage, status: String?, duration: TimeInterval) { + OperationQueue.main.addOperation({ [weak self] in + guard let strongSelf = self else { return } + + strongSelf.setFadeOut(timer: nil) + strongSelf.setGrace(timer: nil) + strongSelf.updateViewHierarchy() + + strongSelf.progress = Float(IHProgressHUDUndefinedProgress) + strongSelf.cancelRingLayerAnimation() + strongSelf.cancelIndefiniteAnimatedViewAnimation() + + if strongSelf.shouldTintImages { + if image.renderingMode != UIImage.RenderingMode.alwaysTemplate { + strongSelf.getImageView().image = image.withRenderingMode(.alwaysTemplate) + strongSelf.getImageView().tintColor = strongSelf.foreGroundColorForStyle() + } + } else { + strongSelf.getImageView().image = image + } + strongSelf.getImageView().isHidden = false + + strongSelf.getStatusLabel().isHidden = status == nil || status?.count == 0 + if let stts = status { + strongSelf.getStatusLabel().text = stts + } + if (strongSelf.graceTimeInterval > 0.0 && strongSelf.getBackGroundView().alpha == 0.0) { + let timer = Timer.init(timeInterval: strongSelf.graceTimeInterval, target: strongSelf, selector: #selector(strongSelf.fadeIn(_:)), userInfo: duration, repeats: false) + strongSelf.setGrace(timer: timer) + RunLoop.main.add(strongSelf.graceTimer!, forMode: .common) + } else { + strongSelf.fadeIn(duration as AnyObject) + } + }) + } + // shows a image + status, use white PNGs with the imageViewSize (default is 28x28 pt) + + private func dismissWithDelay(_ delay: TimeInterval, completion: (() -> Void)?) { + OperationQueue.main.addOperation({ [weak self] in + guard let strongSelf = self else { return } + // Stop timer + strongSelf.setGrace(timer: nil) + // Post notification to inform user + NotificationCenter.default.post(name: NotificationName.IHProgressHUDWillDisappear.getNotificationName(), object: nil, userInfo: strongSelf.notificationUserInfo()) + + // Reset activity count + strongSelf.activityCount = 0 + + let animationsBlock: () -> Void = { + // Shrink HUD a little to make a nice disappear animation + strongSelf.getHudView().transform = strongSelf.getHudView().transform.scaledBy(x: 1 / 1.3, y: 1 / 1.3) + + // Fade out all effects (colors, blur, etc.) + strongSelf.fadeOutEffects() + } + + let completionBlock: (() -> Void) = { + // Check if we really achieved to dismiss the HUD (<=> alpha values are applied) + // and the change of these values has not been cancelled in between e.g. due to a new show + if strongSelf.getBackGroundView().alpha == 0.0 { + // Clean up view hierarchy (overlays) + strongSelf.getControlView().removeFromSuperview() + strongSelf.getBackGroundView().removeFromSuperview() + strongSelf.getHudView().removeFromSuperview() + strongSelf.removeFromSuperview() + + // Reset progress and cancel any running animation + strongSelf.progress = Float(IHProgressHUDUndefinedProgress) + strongSelf.cancelRingLayerAnimation() + strongSelf.cancelIndefiniteAnimatedViewAnimation() + + // Remove observer <=> we do not have to handle orientation changes etc. + NotificationCenter.default.removeObserver(strongSelf) + // Post notification to inform user + //IHProgressHUDDidDisappearNotification + NotificationCenter.default.post(name: NotificationName.IHProgressHUDDidDisappear.getNotificationName(), object: strongSelf, userInfo: strongSelf.notificationUserInfo()) + + // Tell the rootViewController to update the StatusBar appearance + #if os(iOS) + if IHProgressHUD.isNotAppExtension { + if #available(iOS 13.0, *) { + var rootVC:UIViewController? = nil + for scene in UIApplication.shared.connectedScenes { + if scene.activationState == .foregroundActive { + rootVC = ((scene as? UIWindowScene)?.delegate as? UIWindowSceneDelegate)?.window??.rootViewController + break + } + } + rootVC?.setNeedsStatusBarAppearanceUpdate() + } else { + // Fallback on earlier versions + let rootController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController + rootController?.setNeedsStatusBarAppearanceUpdate() + } + + } + #endif + if completion != nil { + completion!() + } + // Run an (optional) completionHandler + + } + } + + // UIViewAnimationOptionBeginFromCurrentState AND a delay doesn't always work as expected + // When UIViewAnimationOptionBeginFromCurrentState is set, animateWithDuration: evaluates the current + // values to check if an animation is necessary. The evaluation happens at function call time and not + // after the delay => the animation is sometimes skipped. Therefore we delay using dispatch_after. + + let dipatchTime = DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) + DispatchQueue.main.asyncAfter(deadline: dipatchTime, execute: { + if strongSelf.fadeOutAnimationDuration > 0 { + UIView.animate(withDuration: strongSelf.fadeOutAnimationDuration, delay: 0, options: [.allowUserInteraction, .curveEaseOut, .beginFromCurrentState], animations: { + animationsBlock() + }) { finished in + completionBlock() + } + }else { + animationsBlock() + completionBlock() + } + }) + + // Inform iOS to redraw the view hierarchy + strongSelf.setNeedsDisplay() + } + ) + } + + @objc private func dismiss() { + dismissWithDelay(0.0, completion: nil) + } + + private func setStatus(_ status: String?) { + getStatusLabel().text = status + updateHUDFrame() + } + + private func updateHUDFrame() { + // Check if an image or progress ring is displayed + let imageUsed: Bool = (getImageView().image) != nil && !((getImageView().isHidden) ) + let progressUsed: Bool = getImageView().isHidden + + // Calculate size of string + var labelRect : CGRect = CGRect.zero + var labelHeight: CGFloat = 0.0 + var labelWidth: CGFloat = 0.0 + + if getStatusLabel().text != nil { + let constraintSize = CGSize(width: 200.0, height: 300.0) + labelRect = getStatusLabel().text?.boundingRect(with: constraintSize, options: [.usesFontLeading, .truncatesLastVisibleLine, .usesLineFragmentOrigin], attributes: [NSAttributedString.Key.font: getStatusLabel().font ?? UIFont.systemFont(ofSize: 15)], context: nil) ?? CGRect.zero + labelHeight = CGFloat(ceilf(Float(labelRect.height ))) + labelWidth = CGFloat(ceilf(Float(labelRect.width ))) + } + + // Calculate hud size based on content + // For the beginning use default values, these + // might get update if string is too large etc. + var hudWidth: CGFloat + var hudHeight: CGFloat + + var contentWidth: CGFloat = 0.0 + var contentHeight: CGFloat = 0.0 + + if (imageUsed || progressUsed) { + if imageUsed { + contentWidth = getImageView().frame.width + contentHeight = getImageView().frame.height + } else { + contentWidth = getIndefiniteAnimatedView().frame.width + contentHeight = getIndefiniteAnimatedView().frame.height + } + } + // |-spacing-content-spacing-| + hudWidth = CGFloat(IHProgressHUDHorizontalSpacing + max(labelWidth, contentWidth) + IHProgressHUDHorizontalSpacing) + + // |-spacing-content-(labelSpacing-label-)spacing-| + hudHeight = CGFloat(IHProgressHUDVerticalSpacing) + labelHeight + contentHeight + CGFloat(IHProgressHUDVerticalSpacing) + if ((getStatusLabel().text != nil) && (imageUsed || progressUsed )) { + // Add spacing if both content and label are used + hudHeight += CGFloat(IHProgressHUDLabelSpacing)//8 [80] + } + + // Update values on subviews + getHudView().bounds = CGRect(x: 0.0, y: 0.0, width: max(minimumSize.width, hudWidth), height: max(minimumSize.height, hudHeight)) + + // Animate value update + CATransaction.begin() + CATransaction.setDisableActions(true) + + // Spinner and image view + var centerY: CGFloat + if getStatusLabel().text != nil { + let yOffset = max(IHProgressHUDVerticalSpacing, (minimumSize.height - contentHeight - CGFloat(IHProgressHUDLabelSpacing) - labelHeight) / 2.0)//12 + centerY = yOffset + contentHeight / 2.0 //26 + } else { + centerY = getHudView().bounds.midY + } + getIndefiniteAnimatedView().center = CGPoint(x: getHudView().bounds.midX, y: centerY) + if CGFloat(progress) != IHProgressHUDUndefinedProgress { + getRingView().center = CGPoint(x: getHudView().bounds.midX , y: centerY) + getBackgroundRingView().center = getRingView().center + } + getImageView().center = CGPoint(x: getHudView().bounds.midX , y: centerY) + // Label + if imageUsed || progressUsed { + if imageUsed { + centerY = getImageView().frame.maxY + IHProgressHUDLabelSpacing + labelHeight / 2.0 + } else { + centerY = getIndefiniteAnimatedView().frame.maxY + IHProgressHUDLabelSpacing + labelHeight / 2.0 + } + } else { + centerY = getHudView().bounds.midY + } + getStatusLabel().frame = labelRect + getStatusLabel().center = CGPoint(x: getHudView().bounds.midX , y: centerY) + CATransaction.commit() + } + + private func registerNotifications() { + #if os(iOS) + if #available(iOS 13.0, *) { + NotificationCenter.default.addObserver(self, selector: #selector(positionHUD(_:)), name: UIDevice.orientationDidChangeNotification, object: nil) + } else { + NotificationCenter.default.addObserver(self, selector: #selector(positionHUD(_:)), name: UIApplication.didChangeStatusBarOrientationNotification, object: nil) + } + NotificationCenter.default.addObserver(self, selector: #selector(self.positionHUD(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(self.positionHUD(_:)), name: UIResponder.keyboardDidHideNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(self.positionHUD(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(self.positionHUD(_:)), name: UIResponder.keyboardDidShowNotification, object: nil) + #endif + NotificationCenter.default.addObserver(self, selector: #selector(self.positionHUD(_:)), name: UIApplication.didBecomeActiveNotification, object: nil) + } + + private func fadeOutEffects() { + if defaultStyle == .custom { + getHudView().effect = nil + } + getHudView().backgroundColor = .clear + getBackGroundView().alpha = 0.0 + + getImageView().alpha = 0.0 + getStatusLabel().alpha = 0.0 + getIndefiniteAnimatedView().alpha = 0.0 + getRingView().alpha = 0 + getBackgroundRingView().alpha = 0 + }// + + private func getBackgroundRingView() -> ProgressAnimatedView { + if backgroundRingView == nil { + backgroundRingView = ProgressAnimatedView.init(frame: .zero) + backgroundRingView?.set(strokeEnd: 1.0) + } + + backgroundRingView?.set(strokeColor: foreGroundColorForStyle().withAlphaComponent(0.1)) + backgroundRingView?.set(strokeThickness: ringThickness) + + var radius : CGFloat = 0.0 + if getStatusLabel().text != nil { + radius = ringRadius + } else { + radius = ringNoTextRadius + } + backgroundRingView?.set(radius: radius) + return backgroundRingView! + } + + private func getRingView() -> ProgressAnimatedView { + if ringView == nil { + ringView = ProgressAnimatedView.init(frame: .zero) + } + + ringView?.set(strokeThickness: ringThickness) + ringView?.set(strokeColor: foreGroundColorForStyle()) + var radius : CGFloat = 0.0 + if getStatusLabel().text != nil { + radius = ringRadius + } else { + radius = ringNoTextRadius + } + ringView?.set(radius: radius) + + return ringView! + } + + private func getImageView() -> UIImageView { + if imageView != nil && imageView?.bounds.size != imageViewSize { + imageView?.removeFromSuperview() + imageView = nil + } + + if imageView == nil { + imageView = UIImageView.init(frame: CGRect.init(x: 0, y: 0, width: imageViewSize.width, height: imageViewSize.height)) + } + if imageView?.superview == nil { + getHudView().contentView.addSubview(imageView!) + } + + return imageView! + } + + private func getStatusLabel() -> UILabel { + if statusLabel == nil { + statusLabel = UILabel.init(frame: .zero) + statusLabel?.backgroundColor = .clear + statusLabel?.adjustsFontSizeToFitWidth = true + statusLabel?.textAlignment = .center + statusLabel?.baselineAdjustment = .alignCenters + statusLabel?.numberOfLines = 0 + } + if statusLabel?.superview == nil && statusLabel != nil { + getHudView().contentView.addSubview(statusLabel!) + } + statusLabel?.textColor = foreGroundColorForStyle() + statusLabel?.font = font + statusLabel?.alpha = 1.0 + statusLabel?.isHidden = false + return statusLabel! + } + + private func fadeInEffects() { + if defaultStyle != .custom { + var blurStyle = UIBlurEffect.Style.light + if defaultStyle == .dark { + blurStyle = UIBlurEffect.Style.light + } + let blurEffect = UIBlurEffect.init(style: blurStyle) + getHudView().effect = blurEffect + + getHudView().backgroundColor = backgroundColorForStyle().withAlphaComponent(0.6) + } else { + getHudView().backgroundColor = backgroundColorForStyle() + } + + getBackGroundView().alpha = 1.0 + getImageView().alpha = 1.0 + getIndefiniteAnimatedView().alpha = 1.0 + getRingView().alpha = 1.0 + getBackgroundRingView().alpha = 1.0 + } + + private func backgroundColorForStyle() -> UIColor { + if defaultStyle == .light { + return .white + } else if defaultStyle == .dark { + return .black + } else { + let color = hudBackgroundColor ?? backgroundColor! + return color + } + } + + private func getFrontWindow() -> UIWindow? { + if IHProgressHUD.isNotAppExtension { + let frontToBackWindows: NSEnumerator = (UIApplication.shared.windows as NSArray).reverseObjectEnumerator() + for window in frontToBackWindows { + guard let win : UIWindow = window as? UIWindow else {return nil} + let windowOnMainScreen: Bool = win.screen == UIScreen.main + let windowIsVisible: Bool = !win.isHidden && (win.alpha > 0) + var windowLevelSupported = false + windowLevelSupported = win.windowLevel >= UIWindow.Level.normal && win.windowLevel <= maxSupportedWindowLevel + + let windowKeyWindow = win.isKeyWindow + + if windowOnMainScreen && windowIsVisible && windowLevelSupported && windowKeyWindow { + return win + } + } + } + return nil + } + + private func getVisibleKeyboardHeight() -> CGFloat { + if IHProgressHUD.isNotAppExtension { + var keyboardWindow : UIWindow? = nil + for testWindow in UIApplication.shared.windows { + if !testWindow.self.isEqual(UIWindow.self) { + keyboardWindow = testWindow + break + } + } + for possibleKeyboard in keyboardWindow?.subviews ?? [] { + var viewName = String.init(describing: possibleKeyboard.self) + if viewName.hasPrefix("UI") { + if viewName.hasSuffix("PeripheralHostView") || viewName.hasSuffix("Keyboard") { + return possibleKeyboard.bounds.height + } else if viewName.hasSuffix("InputSetContainerView") { + for possibleKeyboardSubview: UIView? in possibleKeyboard.subviews { + viewName = String.init(describing: possibleKeyboardSubview.self) + if viewName.hasPrefix("UI") && viewName.hasSuffix("InputSetHostView") { + let convertedRect = possibleKeyboard.convert(possibleKeyboardSubview?.frame ?? CGRect.zero, to: self) + let intersectedRect: CGRect = convertedRect.intersection(bounds) + if !intersectedRect.isNull { + return intersectedRect.height + } + } + } + } + } + } + } + return 0 + } + + #if os(iOS) + private func updateMotionEffectForOrientation(_ orientation: UIInterfaceOrientation) { + let xMotionEffectType: UIInterpolatingMotionEffect.EffectType = orientation.isPortrait ? .tiltAlongHorizontalAxis : .tiltAlongVerticalAxis + let yMotionEffectType: UIInterpolatingMotionEffect.EffectType = orientation.isPortrait ? .tiltAlongVerticalAxis : .tiltAlongHorizontalAxis + updateMotionEffect(forXMotionEffectType: xMotionEffectType, yMotionEffectType: yMotionEffectType) + } + #endif + + private func updateMotionEffect(forXMotionEffectType xMotionEffectType: UIInterpolatingMotionEffect.EffectType, yMotionEffectType: UIInterpolatingMotionEffect.EffectType) { + let effectX = UIInterpolatingMotionEffect(keyPath: "center.x", type: xMotionEffectType) + effectX.minimumRelativeValue = -IHProgressHUDParallaxDepthPoints + effectX.maximumRelativeValue = IHProgressHUDParallaxDepthPoints + + let effectY = UIInterpolatingMotionEffect(keyPath: "center.y", type: yMotionEffectType) + effectY.minimumRelativeValue = -IHProgressHUDParallaxDepthPoints + effectY.maximumRelativeValue = IHProgressHUDParallaxDepthPoints + + let effectGroup = UIMotionEffectGroup() + effectGroup.motionEffects = [effectX, effectY] + + // Clear old motion effect, then add new motion effects + getHudView().motionEffects = [] + getHudView().addMotionEffect(effectGroup) + } + + private func move(to newCenter: CGPoint, rotateAngle angle: CGFloat) { + getHudView().transform = CGAffineTransform(rotationAngle: angle) + guard let container = containerView else { + getHudView().center = CGPoint(x: newCenter.x + offsetFromCenter.horizontal, y: newCenter.y + offsetFromCenter.vertical) + return + } + getHudView().center = CGPoint(x: container.center.x + offsetFromCenter.horizontal, y: container.center.y + offsetFromCenter.vertical) + } +} + +extension IHProgressHUD { + + public class func set(defaultStyle style: IHProgressHUDStyle) { + sharedView.defaultStyle = style + } + + public class func setHUD(backgroundColor color: UIColor) { + sharedView.defaultStyle = .custom + sharedView.hudBackgroundColor = color + } + + public class func set(defaultMaskType maskType: IHProgressHUDMaskType) { + sharedView.defaultMaskType = maskType + } + + public class func set(defaultAnimationType type: IHProgressHUDAnimationType) { + sharedView.defaultAnimationType = type + } + + public class func set(status: String?) { + sharedView.setStatus(status) + } + + public class func set(containerView: UIView?) { + sharedView.containerView = containerView + } // default is window level + + public class func set(minimumSize: CGSize) { + sharedView.minimumSize = minimumSize + } + + // default is CGSizeZero, can be used to avoid resizing for a larger message + + public class func set(ringThickness: CGFloat) { + sharedView.ringThickness = ringThickness + } // default is 2 pt + + public class func set(ringRadius : CGFloat) { + sharedView.ringRadius = ringRadius + }// default is 18 pt + + public class func setRing(noTextRingRadius radius: CGFloat) { + sharedView.ringNoTextRadius = radius + } // default is 24 pt + + public class func set(cornerRadius: CGFloat) { + sharedView.cornerRadius = cornerRadius + }// default is 14 pt + + public class func set(borderColor color : UIColor) { + sharedView.getHudView().layer.borderColor = color.cgColor + + }// default is nil + + public class func set(borderWidth width: CGFloat) { + sharedView.getHudView().layer.borderWidth = width + }// default is 0 + + public class func set(font: UIFont) { + sharedView.font = font + } // default is [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline] + + public class func set(foregroundColor color: UIColor) { + sharedView.foregroundColor = color + // sharedView.defaultStyle = .custom + } + // default is [UIColor blackColor], only used for ProgressHUDStyleCustom + + public class func set(backgroundColor color: UIColor) { + sharedView.backgroundColor = color + sharedView.defaultStyle = .custom + } // default is [UIColor whiteColor], only used for ProgressHUDStyleCustom + + public class func set(backgroundLayerColor color: UIColor) { + sharedView.backgroundLayerColor = color + } // default is [UIColor colorWithWhite:0 alpha:0.5], only used for ProgressHUDMaskTypeCustom + + public class func set(imageViewSize size: CGSize) { + sharedView.imageViewSize = size + } // default is 28x28 pt + + public class func set(shouldTintImages: Bool) { + sharedView.shouldTintImages = shouldTintImages + } // default is YES + + public class func set(infoImage image: UIImage) { + sharedView.infoImage = image + } // default is the bundled info image provided by Freepik + + public class func setSuccessImage(successImage image: UIImage) { + sharedView.successImage = image + } // default is the bundled success image provided by Freepik + + public class func setErrorImage(errorImage image: UIImage) { + sharedView.errorImage = image + } // default is the bundled error image provided by Freepik + + public class func set(viewForExtension view: UIView) { + IHProgressHUD.isNotAppExtension = false + sharedView.viewForExtension = view + }// default is nil, only used if #define SV_APP_EXTENSIONS is set + + public class func set(graceTimeInterval interval: TimeInterval) { + sharedView.graceTimeInterval = interval + } // default is 0 seconds + + public class func set(minimumDismiss interval: TimeInterval) { + sharedView.minimumDismissTimeInterval = interval + } // default is 5.0 seconds + + public class func set(maximumDismissTimeInterval interval: TimeInterval) { + sharedView.maximumDismissTimeInterval = interval + } // default is infinite + + public class func setFadeInAnimationDuration(fadeInAnimationDuration duration: TimeInterval) { + sharedView.fadeInAnimationDuration = duration + } // default is 0.15 seconds + + public class func setFadeOutAnimationDuration(fadeOutAnimationDuration duration: TimeInterval) { + sharedView.fadeOutAnimationDuration = duration + } // default is 0.15 seconds + + public class func setMaxSupportedWindowLevel(maxSupportedWindowLevel windowLevel: UIWindow.Level) { + sharedView.maxSupportedWindowLevel = windowLevel + } // default is UIWindowLevelNormal + + public class func setHapticsEnabled(hapticsEnabled: Bool) { + sharedView.hapticsEnabled = hapticsEnabled + } // default is NO + + + // MARK: - Show Methods + public class func show() { + show(withStatus: nil) + } + + public class func show(withStatus status: String?) { + show(progress: IHProgressHUDUndefinedProgress, status: status) + } + + public class func show(progress: CGFloat) { + show(progress: progress, status: nil) + } + + public class func show(progress: CGFloat, status: String?) { + sharedView.showProgress(progress: Float(progress), status: status) + } + + public class func setOffsetFromCenter(_ offset: UIOffset) { + sharedView.offsetFromCenter = offset + } + + public class func resetOffsetFromCenter() { + setOffsetFromCenter(.zero) + } + + public class func popActivity() { + if sharedView.activityCount > 0 { + sharedView.activityCount -= 1 + } + if sharedView.activityCount == 0 { + sharedView.dismiss() + } + } // decrease activity count, if activity count == 0 the HUD is dismissed + + public class func dismiss() { + dismissWithDelay(0.0) + } + + public class func dismissWithCompletion(_ completion: (() -> Void)?) { + dismissWithDelay(0.0, completion: completion) + } + + public class func dismissWithDelay(_ delay: TimeInterval) { + dismissWithDelay(delay, completion: nil) + } + + public class func dismissWithDelay(_ delay: TimeInterval, completion: (() -> Void)?) { + sharedView.dismissWithDelay(delay, completion: completion) + } + + public class func isVisible() -> Bool { + return sharedView.getBackGroundView().alpha > 0.0 + } + + public class func displayDurationForString(_ string:String?) -> TimeInterval { + let minimum = max(CGFloat(string?.count ?? 0) * 0.06 + 0.5, CGFloat(sharedView.minimumDismissTimeInterval)) + return TimeInterval(min(minimum, CGFloat(sharedView.maximumDismissTimeInterval))) + } + + public class func showInfowithStatus(_ status: String?) { + showImage(sharedView.infoImage, status: status) + #if os(iOS) + if #available(iOS 10.0, *) { + sharedView.hapticGenerator?.notificationOccurred(.warning) + } + #endif + } + + public class func showImage(_ image: UIImage, status: String?) { + let displayInterval = displayDurationForString(status) + sharedView.show(image: image, status: status, duration: displayInterval) + } + + public class func showSuccesswithStatus(_ status: String?) { + showImage(sharedView.successImage, status: status) + #if os(iOS) + if #available(iOS 10.0, *) { + sharedView.hapticGenerator?.notificationOccurred(.success) + } + #endif + } + + public class func showError(withStatus status: String?) { + showImage(sharedView.errorImage, status: status) + #if os(iOS) + if #available(iOS 10.0, *) { + sharedView.hapticGenerator?.notificationOccurred(.error) + } + #endif + } +} +//MARK: - +extension IHProgressHUD { + private func setGrace(timer: Timer?) { + if (graceTimer != nil) { + graceTimer?.invalidate() + graceTimer = nil + } else { + if timer != nil { + graceTimer = timer + } + } + } + + private func setFadeOut(timer: Timer?) { + if (fadeOutTimer != nil) { + fadeOutTimer?.invalidate() + fadeOutTimer = nil + } + if timer != nil { + fadeOutTimer = timer + } + } +} + +//MARK: - Instance Getter Methods +extension IHProgressHUD { + private func foreGroundColorForStyle() -> UIColor { + guard let color = foregroundColor else { + if defaultStyle == .light { + return .black + } else if defaultStyle == .dark { + return .white + } else { + return .black + } + } + return color + } + + private func getHudView() -> UIVisualEffectView { + if hudView == nil { + let tmphudView = UIVisualEffectView() + tmphudView.layer.masksToBounds = true + tmphudView.autoresizingMask = [.flexibleBottomMargin, .flexibleTopMargin, .flexibleRightMargin, .flexibleLeftMargin] + hudView = tmphudView + hudView?.accessibilityLabel = "HUD View" + } + + if hudView?.superview == nil { + self.addSubview(hudView!) + } + + hudView?.layer.cornerRadius = cornerRadius + return hudView! + } + + private func getBackGroundView() -> UIView { + if backgroundView == nil { + backgroundView = UIView() + backgroundView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] + } + if backgroundView?.superview == nil { + insertSubview(self.backgroundView!, belowSubview: getHudView()) + } + // Update styling + if defaultMaskType == .gradient { + if (backgroundRadialGradientLayer == nil) { + backgroundRadialGradientLayer = RadialGradientLayer() + } + if (backgroundRadialGradientLayer?.superlayer == nil) { + backgroundView!.layer.insertSublayer(backgroundRadialGradientLayer!, at: 0) + } + } else { + if ((backgroundRadialGradientLayer != nil) && (backgroundRadialGradientLayer?.superlayer != nil)) { + backgroundRadialGradientLayer?.removeFromSuperlayer() + } + if defaultMaskType == .black { + backgroundView?.backgroundColor = UIColor(white: 0, alpha: 0.4) + } else if defaultMaskType == .custom { + backgroundView?.backgroundColor = backgroundLayerColor + } else { + backgroundView?.backgroundColor = UIColor.clear + } + } + + // Update frame + if backgroundView != nil { + backgroundView?.frame = bounds + } + if backgroundRadialGradientLayer != nil { + backgroundRadialGradientLayer?.frame = bounds + + // Calculate the new center of the gradient, it may change if keyboard is visible + var gradientCenter: CGPoint = center + gradientCenter.y = (bounds.size.height - visibleKeyboardHeight) / 2 + backgroundRadialGradientLayer?.gradientCenter = gradientCenter + backgroundRadialGradientLayer?.setNeedsDisplay() + } + return backgroundView! + } + + private func getControlView() -> UIControl { + if controlView == nil { + controlView = UIControl.init() + controlView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] + controlView?.backgroundColor = .clear + controlView?.isUserInteractionEnabled = true + controlView?.addTarget(self, action: #selector(controlViewDidReceiveTouchEvent(_:for:)), for: .touchDown) + } + if IHProgressHUD.isNotAppExtension { + if let windowBounds : CGRect = UIApplication.shared.delegate?.window??.bounds { + controlView?.frame = windowBounds + } + } + else { + controlView?.frame = UIScreen.main.bounds + } + return controlView! + } + + private func loadImageBundle(named imageName:String) -> UIImage? { + var imageBundle = Bundle.init(for: IHProgressHUD.self) + if let resourcePath = imageBundle.path(forResource: "IHProgressHUD", ofType: "bundle") { + if let resourcesBundle = Bundle(path: resourcePath) { + imageBundle = resourcesBundle + } + } + + return (UIImage(named: imageName, in: imageBundle, compatibleWith: nil)) + } +} diff --git a/AutoCat/ThirdParty/IHProgressHUD/IndefiniteAnimatedView.swift b/AutoCat/ThirdParty/IHProgressHUD/IndefiniteAnimatedView.swift new file mode 100755 index 0000000..0911028 --- /dev/null +++ b/AutoCat/ThirdParty/IHProgressHUD/IndefiniteAnimatedView.swift @@ -0,0 +1,197 @@ +// +// Converted to Swift 4 by Swiftify v4.2.29618 - https://objectivec2swift.com/ +// +// IndefiniteAnimatedView.swift +// SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD +// +// Original Copyright (c) 2014-2018 Guillaume Campagna. All rights reserved. +// Modified Copyright © 2018 Ibrahim Hassan. All rights reserved. +// + +import UIKit + +class IndefiniteAnimatedView : UIView { + + private var activityIndicator : UIActivityIndicatorView? + private var strokeThickness : CGFloat? + private var strokeColor : UIColor? + private var indefinteAnimatedLayer : CAShapeLayer? + private var radius : CGFloat? + + override init(frame: CGRect) { + super.init(frame: frame) + if self.superview != nil { + layoutAnimatedLayer() + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +//MARK: - Setter Functions +extension IndefiniteAnimatedView { + + func setIndefinite(radius: CGFloat) { + if (self.radius != radius) { + self.radius = radius + + self.getIndefinteAnimatedLayer().removeFromSuperlayer() + self.indefinteAnimatedLayer = nil + + if superview != nil { + layoutAnimatedLayer() + } + } + } + + func setIndefinite(strokeThickness : CGFloat) { + self.strokeThickness = strokeThickness + if let strkthickness = self.strokeThickness { + getIndefinteAnimatedLayer().lineWidth = strkthickness + } + } + + func setIndefinite(strokeColor: UIColor) { + self.strokeColor = strokeColor + getIndefinteAnimatedLayer().strokeColor = strokeColor.cgColor + } + +} + +//MARK: - Getter Functions +extension IndefiniteAnimatedView { + private func getIndefinteAnimatedLayer() -> CAShapeLayer { + if self.indefinteAnimatedLayer != nil { + return self.indefinteAnimatedLayer! + } else { + let localRingRadius : CGFloat = radius ?? 18 + let localStrokeThickness : CGFloat = strokeThickness ?? 2 + let localStrokeColor : UIColor = strokeColor ?? UIColor.black + + let arcCenter = CGPoint(x: localRingRadius + localStrokeThickness / 2 + 5, y: localRingRadius + localStrokeThickness / 2 + 5) + let smoothedPath = UIBezierPath(arcCenter: arcCenter, radius: localRingRadius, startAngle: -CGFloat.pi / 2, endAngle: CGFloat.pi + CGFloat.pi / 2, clockwise: true) + + indefinteAnimatedLayer = CAShapeLayer() + indefinteAnimatedLayer?.contentsScale = UIScreen.main.scale + indefinteAnimatedLayer?.frame = CGRect.init(x: 0, y: 0, width: arcCenter.x * 2, height: arcCenter.y * 2) + indefinteAnimatedLayer?.fillColor = UIColor.clear.cgColor + indefinteAnimatedLayer?.strokeColor = localStrokeColor.cgColor + indefinteAnimatedLayer?.lineWidth = localStrokeThickness + indefinteAnimatedLayer?.lineCap = CAShapeLayerLineCap.round + indefinteAnimatedLayer?.lineJoin = CAShapeLayerLineJoin.bevel + indefinteAnimatedLayer?.path = smoothedPath.cgPath + + let maskLayer = CALayer() + let image = loadImageBundle(named: "angle-mask")! + maskLayer.contents = image.cgImage + maskLayer.frame = indefinteAnimatedLayer!.bounds + indefinteAnimatedLayer?.mask = maskLayer + + let animationDuration = TimeInterval.init(1) + let linearCurve = CAMediaTimingFunction.init(name: .linear) + let animation = CABasicAnimation.init(keyPath: "transform.rotation") + animation.fromValue = 0 + animation.toValue = CGFloat.pi * 2 + animation.duration = animationDuration + animation.timingFunction = linearCurve + animation.isRemovedOnCompletion = false + animation.repeatCount = .infinity + animation.fillMode = .forwards + animation.autoreverses = false + indefinteAnimatedLayer?.mask?.add(animation, forKey: "rotate") + + + let animationGroup = CAAnimationGroup.init() + animationGroup.duration = animationDuration + animationGroup.repeatCount = .infinity + animationGroup.isRemovedOnCompletion = false + animationGroup.timingFunction = linearCurve + + let strokeStartAnimation = CABasicAnimation.init(keyPath: "strokeStart") + strokeStartAnimation.duration = animationDuration + strokeStartAnimation.fromValue = 0.015 + strokeStartAnimation.toValue = 0.0001 + + animationGroup.animations = [strokeStartAnimation] + indefinteAnimatedLayer?.add(animationGroup, forKey: "progress") + } + return self.indefinteAnimatedLayer! + } +} + +//MARK: - ActivityIndicatorView Functions +extension IndefiniteAnimatedView { + + func removeAnimationLayer() { + for view in self.subviews { + if let activityView = view as? UIActivityIndicatorView { + activityView.removeFromSuperview() + } + } + getIndefinteAnimatedLayer().removeFromSuperlayer() + } + + func startAnimation() { + if let activityIndicator = activityIndicator { + self.addSubview(activityIndicator) + activityIndicator.frame = CGRect.init(x: 8, y: 8, width: self.frame.size.width - 16, height: self.frame.size.height - 16) + } + } + + func stopActivityIndicator() { + activityIndicator?.stopAnimating() + } + + func setActivityIndicator(color: UIColor) { + activityIndicator = UIActivityIndicatorView.init(style: UIActivityIndicatorView.Style.large) + activityIndicator?.hidesWhenStopped = true + activityIndicator?.startAnimating() + activityIndicator?.color = color + } +} +//MARK: - +extension IndefiniteAnimatedView { + override func willMove(toSuperview newSuperview: UIView?) { + if let _ = newSuperview { + layoutAnimatedLayer() + } else { + getIndefinteAnimatedLayer().removeFromSuperlayer() + indefinteAnimatedLayer = nil + } + } + + private func layoutAnimatedLayer() { + let calayer = getIndefinteAnimatedLayer() + self.layer.addSublayer(calayer) + let widthDiff: CGFloat = bounds.width - layer.bounds.width + let heightDiff: CGFloat = bounds.height - layer.bounds.height + let xPos = bounds.width - layer.bounds.width / 2 - widthDiff / 2 + let yPos = bounds.height - layer.bounds.height / 2 - heightDiff / 2 + calayer.position = CGPoint.init(x: xPos, y: yPos) + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + let localRadius : CGFloat = radius ?? 18 + let localStrokeThickness : CGFloat = strokeThickness ?? 2 + for view in self.subviews { + if let _ = view as? UIActivityIndicatorView { + return CGSize.init(width: 50, height: 50) + } + } + return CGSize.init(width: (localRadius + localStrokeThickness / 2 + 5) * 2, height: (localRadius + localStrokeThickness / 2 + 5) * 2) + } + + private func loadImageBundle(named imageName:String) -> UIImage? { + var imageBundle = Bundle.init(for: IndefiniteAnimatedView.self) + if let resourcePath = imageBundle.path(forResource: "IHProgressHUD", ofType: "bundle") { + if let resourcesBundle = Bundle(path: resourcePath) { + imageBundle = resourcesBundle + } + } + + return (UIImage(named: imageName, in: imageBundle, compatibleWith: nil)) + } +} + diff --git a/AutoCat/ThirdParty/IHProgressHUD/ProgressAnimatedView.swift b/AutoCat/ThirdParty/IHProgressHUD/ProgressAnimatedView.swift new file mode 100755 index 0000000..5437310 --- /dev/null +++ b/AutoCat/ThirdParty/IHProgressHUD/ProgressAnimatedView.swift @@ -0,0 +1,128 @@ +// +// Converted to Swift 4 by Swiftify v4.2.29618 - https://objectivec2swift.com/ +// +// IndefiniteAnimatedView.swift +// SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD +// +// Original Copyright (c) 2014-2018 Guillaume Campagna. All rights reserved. +// Modified Copyright © 2018 Ibrahim Hassan. All rights reserved. +// + +import UIKit + +class ProgressAnimatedView: UIView { + + private var radius : CGFloat? + private var strokeThickness : CGFloat? + private var strokeColor : UIColor? + private var strokeEnd : CGFloat? + private var ringAnimatedLayer : CAShapeLayer? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func willMove(toSuperview newSuperview: UIView?) { + if let _ = newSuperview { + layoutAnimatedLayer() + } else { + getRingAnimatedLayer().removeFromSuperlayer() + ringAnimatedLayer = nil + } + } + + func layoutAnimatedLayer() { + let rlayer = getRingAnimatedLayer() + layer.addSublayer(rlayer) + let widthDiff = bounds.width - layer.bounds.width + let heightDiff = bounds.height - layer.bounds.height + let layerPositionX = bounds.width - layer.bounds.width / 2 - widthDiff / 2 + let layerPositionY = bounds.height - layer.bounds.height / 2 - heightDiff / 2 + rlayer.position = CGPoint.init(x: layerPositionX, y: layerPositionY) + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + let localRadius : CGFloat = radius ?? 18 + let localStrokeThickness : CGFloat = strokeThickness ?? 2 + return CGSize(width: (localRadius + localStrokeThickness / 2 + 5) * 2, height: (localRadius + localStrokeThickness / 2 + 5) * 2) + } + +} + +//MARK: - Setter +extension ProgressAnimatedView { + @objc public func set(radius: CGFloat) { + if radius != self.radius { + self.radius = radius + + getRingAnimatedLayer().removeFromSuperlayer() + ringAnimatedLayer = nil + + if superview != nil { + layoutAnimatedLayer() + } + } + } + + func set(strokeThickness : CGFloat) { + self.strokeThickness = strokeThickness + getRingAnimatedLayer().lineWidth = strokeThickness + + if superview != nil { + layoutAnimatedLayer() + } + + } + + func set(strokeEnd: CGFloat) { + self.strokeEnd = strokeEnd + getRingAnimatedLayer().strokeEnd = strokeEnd + + if superview != nil { + layoutAnimatedLayer() + } + + } + + func set(strokeColor: UIColor) { + + self.strokeColor = strokeColor + self.getRingAnimatedLayer().strokeColor = strokeColor.cgColor + + if superview != nil { + layoutAnimatedLayer() + } + } +} + +//MARK: - Getter +extension ProgressAnimatedView { + private func getRingAnimatedLayer() -> CAShapeLayer { + if self.ringAnimatedLayer != nil { + return self.ringAnimatedLayer! + } else { + let localStrokeThickness: CGFloat = strokeThickness ?? 2 + let localRingRadius: CGFloat = radius ?? 18 + + let arcCenter = CGPoint(x: localRingRadius + localStrokeThickness / 2 + 5, y: localRingRadius + localStrokeThickness / 2 + 5) + let smoothedPath = UIBezierPath(arcCenter: arcCenter, radius: localRingRadius, startAngle: -CGFloat.pi / 2, endAngle: CGFloat.pi + CGFloat.pi / 2, clockwise: true) + + let _ringAnimatedLayer = CAShapeLayer() + _ringAnimatedLayer.contentsScale = UIScreen.main.scale + _ringAnimatedLayer.frame = CGRect(x: 0.0, y: 0.0, width: arcCenter.x * 2, height: arcCenter.y * 2) + _ringAnimatedLayer.fillColor = UIColor.clear.cgColor + _ringAnimatedLayer.strokeColor = strokeColor?.cgColor + _ringAnimatedLayer.lineWidth = localStrokeThickness + _ringAnimatedLayer.lineCap = .round + _ringAnimatedLayer.lineJoin = .bevel + _ringAnimatedLayer.path = smoothedPath.cgPath + self.ringAnimatedLayer = _ringAnimatedLayer + } + return self.ringAnimatedLayer! + } + +} diff --git a/AutoCat/ThirdParty/IHProgressHUD/RadialGradientLayer.swift b/AutoCat/ThirdParty/IHProgressHUD/RadialGradientLayer.swift new file mode 100755 index 0000000..1be0c08 --- /dev/null +++ b/AutoCat/ThirdParty/IHProgressHUD/RadialGradientLayer.swift @@ -0,0 +1,27 @@ +// +// Converted to Swift 4 by Swiftify v4.2.29618 - https://objectivec2swift.com/ +// +// IndefiniteAnimatedView.swift +// SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD +// +// Original Copyright (c) 2014-2018 Guillaume Campagna. All rights reserved. +// Modified Copyright © 2018 Ibrahim Hassan. All rights reserved. +// + +import QuartzCore + +class RadialGradientLayer: CALayer { + var gradientCenter = CGPoint.zero + override func draw(in context: CGContext) { + super.draw(in: context) + let locationsCount = 2 + let locations : [CGFloat] = [0.0, 1.0] + let colors : [CGFloat] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.75] + let colorSpace = CGColorSpaceCreateDeviceRGB() + if let gradient = CGGradient.init(colorSpace: colorSpace, colorComponents: colors, locations: locations, count: locationsCount) { + let radius = min(bounds.size.width, bounds.size.height) + + context.drawRadialGradient(gradient, startCenter: gradientCenter, startRadius: 0, endCenter: gradientCenter, endRadius: radius, options: CGGradientDrawingOptions.drawsAfterEndLocation) + } + } +}