From 92c610592ac25b32e2bf49bc7e5edfb67e8ae303 Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Tue, 12 May 2020 15:36:19 +0300 Subject: [PATCH] Enabled mac catalyst --- AutoCat.xcodeproj/project.pbxproj | 101 +- .../xcshareddata/swiftpm/Package.resolved | 20 +- AutoCat/AppDelegate.swift | 9 +- AutoCat/AutoCat.entitlements | 10 + AutoCat/Base.lproj/Main.storyboard | 40 +- AutoCat/Controllers/AuthController.swift | 11 +- AutoCat/Controllers/CheckController.swift | 7 +- AutoCat/Controllers/ReportController.swift | 3 +- .../ATGMediaBrowser/ContentTransformers.swift | 236 +++ .../DismissAnimationController.swift | 288 ++++ .../MediaBrowserViewController.swift | 1019 +++++++++++++ .../ATGMediaBrowser/MediaContentView.swift | 315 ++++ AutoCat/ThirdParty/IHProgressHUD/.gitkeep | 0 .../IHProgressHUD.bundle/angle-mask@1x.png | Bin 0 -> 4816 bytes .../IHProgressHUD.bundle/angle-mask@2x.png | Bin 0 -> 14646 bytes .../IHProgressHUD.bundle/angle-mask@3x.png | Bin 0 -> 26585 bytes .../IHProgressHUD.bundle/error@1x.png | Bin 0 -> 409 bytes .../IHProgressHUD.bundle/error@2x.png | Bin 0 -> 873 bytes .../IHProgressHUD.bundle/error@3x.png | Bin 0 -> 752 bytes .../IHProgressHUD.bundle/info@1x.png | Bin 0 -> 662 bytes .../IHProgressHUD.bundle/info@2x.png | Bin 0 -> 1458 bytes .../IHProgressHUD.bundle/info@3x.png | Bin 0 -> 2164 bytes .../IHProgressHUD.bundle/success@1x.png | Bin 0 -> 490 bytes .../IHProgressHUD.bundle/success@2x.png | Bin 0 -> 912 bytes .../IHProgressHUD.bundle/success@3x.png | Bin 0 -> 1349 bytes .../IHProgressHUD/IHProgressHUD.swift | 1312 +++++++++++++++++ .../IndefiniteAnimatedView.swift | 197 +++ .../IHProgressHUD/ProgressAnimatedView.swift | 128 ++ .../IHProgressHUD/RadialGradientLayer.swift | 27 + 29 files changed, 3633 insertions(+), 90 deletions(-) create mode 100644 AutoCat/AutoCat.entitlements create mode 100644 AutoCat/ThirdParty/ATGMediaBrowser/ContentTransformers.swift create mode 100644 AutoCat/ThirdParty/ATGMediaBrowser/DismissAnimationController.swift create mode 100644 AutoCat/ThirdParty/ATGMediaBrowser/MediaBrowserViewController.swift create mode 100644 AutoCat/ThirdParty/ATGMediaBrowser/MediaContentView.swift create mode 100644 AutoCat/ThirdParty/IHProgressHUD/.gitkeep create mode 100644 AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/angle-mask@1x.png create mode 100644 AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/angle-mask@2x.png create mode 100644 AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/angle-mask@3x.png create mode 100644 AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/error@1x.png create mode 100644 AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/error@2x.png create mode 100644 AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/error@3x.png create mode 100644 AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/info@1x.png create mode 100644 AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/info@2x.png create mode 100644 AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/info@3x.png create mode 100644 AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/success@1x.png create mode 100644 AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/success@2x.png create mode 100644 AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.bundle/success@3x.png create mode 100755 AutoCat/ThirdParty/IHProgressHUD/IHProgressHUD.swift create mode 100755 AutoCat/ThirdParty/IHProgressHUD/IndefiniteAnimatedView.swift create mode 100755 AutoCat/ThirdParty/IHProgressHUD/ProgressAnimatedView.swift create mode 100755 AutoCat/ThirdParty/IHProgressHUD/RadialGradientLayer.swift 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 0000000000000000000000000000000000000000..ffecbd448c0a3e7e44f60d152db622763eb27804 GIT binary patch literal 4816 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..452458effa72b5dc46d3c143e7648db424151138 GIT binary patch literal 14646 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bf8e5e42c9b77f08d82183544cb48fa3422b3c03 GIT binary patch literal 26585 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5c11ed00fc95704ec8e18f23eea7f25cad873f8f GIT binary patch literal 873 zcmV-v1D5=WP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91IG_Un1ONa40RR91H~;_u01a1YQvd)0(@8`@RA>e5T3bp4K@40Ke1XUD z%kzjIzCDcZLkS*55K%-^unlcd*?D!(WMl)OH@)3GsZ^5QU3Rfp%Q^y~Bd|r@9hiM) zdT9D$`fWM~*)bKv%Hg&NSa}Z#GgOn!qRcmir3d8wpWS!9k51ZOvE$Ul?4cZ1bWMfl2sK zB+DW)IhZ+4F%>ODZwe->`4WN2;> zF^QV20TO4bZdsf;4#F5rj?Nr|L`PF~QG!VY4$m6BbJt)}fvZ|`%pBV#nN;8=VU9t1 zOm;~r6{ty>W00!5BKmD=(qpcAw`34ab{{6gGsmDkCIOxWbE*@EW3H{^c}*(j09FNp z&ktdP1vH8L+cp4TUa^d|;h8fZ|A|pcKeH zsh`elHL7koG%dXcd+A!OCKYqFz$MTg$l^?T-Lgc@B{1HoKo;$;iaFFr@hEx-m(QwF zljxS}p2PguqB=Kx>+Oq5jj@^IAR=oNlbuT?5_1f4Fq08#jzO{QG1=G`upmm!F(|e@ zCRc*IvQzSpNXYknJ~@yL-?%0(bN~2f!`#Ltes21nZwU4pzQITMnk2`t*hsU>5r2q1 zHvKYvGW{@}gCJc`Oz{WbXVW*+wX7pBhY0)vJ=%=xUShwC00000NkvXXu0mjfr9O1x literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..91add0971f95a29474a4ef78012bcab927acfd28 GIT binary patch literal 752 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 M07*qoM6N<$f|p&cQ2+n{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6539bda822cc755f1688d0970932e636256338f8 GIT binary patch literal 2164 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 T@yzxND4acA{an^LB{Ts5hA+LD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b70ab2b3a4364f4669faab8f2721ddb5e9152cc0 GIT binary patch literal 912 zcmV;B18@9^P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91IG_Un1ONa40RR91H~;_u01a1YQvd)0`bk7VRA>e5nM;UHVHn59osnA> zE*r{i7Lp>DQWh4bSXo%Gn1wQBL}r$7w%Dbfg}5lADD;|L7NvF}=RJ(7cN zhGp;wzCoB6hDjIvF3}Oj3=zq7mc%632W=1{24k@ket}i*7`ZOj5TQ%)2bkP>^2QKm zNNj@dU~-qp$3lo`!QvB`+%@tEA60Xzqjdq%zpLd0Awo`A`{BwrH342fBw`^@CtkT*k!n1+RB zNRxXmWo-#NsNL+*k6J5N1e>fs0^rTCi?`5HSji9xyq5hFP?Z zK<22E)7owqga|EI4}h=SZrUM!FS6CRms2J66GVvEj>S(f!IR{=7ej<*M=z(@Q?qC; z(MYcwVFT!f&pTQ|3=U3woXwQJEg=92~c}YE=hF9PWmea>8*SG#} zFsB0jmfeLUkAs&yM0+5WT*&?}T%XG4L(M`mr@>3^r~N;boX37oU0+&_FNT_jBu{{s z)a6>v#50raAG$tt|LIaKOh~dDyyQOG{dtX=&UW|QzFLhpL*YWR>nX@e{_AqpGwrt9 z=L5MO4}}d$o&l5WBkzQvkPWTIuY+x?x0UtTjf&XTt6VqOiR)96zLMi;Uvb-fCU1qh z303ken4~JX7Y2du*9&0ZX~DV;^j%$rD(NMylKurjD})_&WtBNuN%nwMEBm>J@BVgw zX1XbMb2O)S4zhAb_*;J-BgM8Wot-M_C3TsW^CD0c$$?G+s$?5z!qIOjdMuPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91RGcFeylm`ti5LM@64Gqv*z^b<>myOfD>>6PQVE` z0Vm)DoIqI!^kJgkpvUMedIP!TB+#32yU@Q0ozGdT^26#NurmSDpgyTQnVbMN1z-y5 zww(1UJ?zU2mdvHFL|Aez5RY)qxe zYbxW!9saphrq2%h9 z+aQ|PFZ|4iI-UxFK}gq>2GP8(JwHNj5d;R3dlVT&^SUcq7*WSlA)x#3KafF8`Y)VN z907HP_GiFcA6Lq=)otIC0duqCi6fwH`*#^Iza_5s-69BVAvbxhwC&x*b>zB55crB* z1DN#th4&qWLCbuOPPw0#nEhJ-WC}T>VbB2m%wyRcGCfNb~oI zsT1!OL0~Modgfyg%?t3G5p_Hj0;5o<+pe$vbSD%?;APUYy6x}9mGW#|{+vPf^)|0- z&kr3>906UJA4dk!yzYwTBDV+v>b4(71~KWEM%3x_)WPgQe;oY<>9(zirq0l>C<)QF z*A;ETzo#a;reG6P&vI{Sjs|mM!RGq^bV9dls|zwwb<=-91~BP&5HDo6{kMz>uO!~x zww(ziA=>tj#0v>iz+OV;B2cbt7Cy1A}+g=Om zvQ8xW0kuPqF3u6x?PjS>&4?A4CiNm1(5g8EqpOmZ?cLfv*XjmLFB{X&)y)JBv(f^?*Q+Kw5IXRN@b>k9Cs zcGNA!Q2iJEDoQV2Q1z#RSkr^ETPR&mU;U}E#mbsLf{9;4X$XS4C}L4M;{OWN(T~zQ z5p{Y*YI~vT@+Wlj&`Nw(k06v|5ewgNP#V%RVo9hk@LGQ%Oy}u!(VE#zse4UOucI_9L7j*C zApN;D{k~p*jobnRMiaY%($MrnqyJ;3<9fXnxz!+`Tf`955X|q%t*cSX-+E3!P@7Q* zqQ!pV{oc>>-L-*$?trqO9^k#wblcmo)-FqVoju(|c2vqALb`Nv%Sk{->S6Q{?LwvP zwljA(C*TB}fD>>6PQVE`0Vm)DoPZN>0#3jQH~}Zn#RUEXk_`GvI4v`+00000NkvXX Hu0mjfpde?D literal 0 HcmV?d00001 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) + } + } +}