diff --git a/twackup-gui/Twackup.xcodeproj/project.pbxproj b/twackup-gui/Twackup.xcodeproj/project.pbxproj index 5f8e4e4..f7d0cfa 100644 --- a/twackup-gui/Twackup.xcodeproj/project.pbxproj +++ b/twackup-gui/Twackup.xcodeproj/project.pbxproj @@ -55,8 +55,6 @@ 0B88C3172930CE3900BA5312 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B88C3162930CE3900BA5312 /* Metadata.swift */; }; 0B88C31C2930DE8C00BA5312 /* KeyValueLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B88C31B2930DE8C00BA5312 /* KeyValueLabelView.swift */; }; 0B88C31E2930DEA200BA5312 /* PackageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B88C31D2930DEA200BA5312 /* PackageDetailView.swift */; }; - 0BAAD9922937BE360089C8FB /* RJTHudProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BAAD98F2937BE350089C8FB /* RJTHudProgressView.m */; }; - 0BAAD9932937BE360089C8FB /* RJTHud.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BAAD9902937BE350089C8FB /* RJTHud.m */; }; 0BAAD99729388D310089C8FB /* DebsListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BAAD99629388D310089C8FB /* DebsListVC.swift */; }; 0BAAD99C293893EF0089C8FB /* DpkgDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BAAD99B293893EF0089C8FB /* DpkgDataProvider.swift */; }; 0BAC44D429562BDE0038074C /* DebsListModel+DZNEmptyDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BAC44D329562BDE0038074C /* DebsListModel+DZNEmptyDataSet.swift */; }; @@ -70,6 +68,9 @@ 0BE87E902C1E0565009C95A7 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE87E8F2C1E0565009C95A7 /* Result.swift */; }; 0BE87E932C1E05CE009C95A7 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 0BE87E922C1E05CE009C95A7 /* NukeUI */; }; 0BE87E952C1E2214009C95A7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 0BE87E942C1E2214009C95A7 /* Localizable.xcstrings */; }; + 0BE87E982C1E23A2009C95A7 /* Hud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE87E962C1E23A2009C95A7 /* Hud.swift */; }; + 0BE87E992C1E23A2009C95A7 /* HudCircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE87E972C1E23A2009C95A7 /* HudCircularProgressView.swift */; }; + 0BE87E9D2C1E23D3009C95A7 /* UIWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE87E9C2C1E23D3009C95A7 /* UIWindow.swift */; }; 0BF07A502941027800EEC8FC /* SplitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF07A4F2941027800EEC8FC /* SplitController.swift */; }; 0BF07A62294335F700EEC8FC /* FFILogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF07A61294335F700EEC8FC /* FFILogger.swift */; }; 0BF07A6429433BE100EEC8FC /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF07A6329433BE100EEC8FC /* LogViewController.swift */; }; @@ -133,10 +134,6 @@ 0B88C31B2930DE8C00BA5312 /* KeyValueLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueLabelView.swift; sourceTree = ""; }; 0B88C31D2930DEA200BA5312 /* PackageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageDetailView.swift; sourceTree = ""; }; 0B9EB5E1294B8C7700DED54D /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; - 0BAAD98E2937BE350089C8FB /* RJTHudProgressView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RJTHudProgressView.h; sourceTree = ""; }; - 0BAAD98F2937BE350089C8FB /* RJTHudProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RJTHudProgressView.m; sourceTree = ""; }; - 0BAAD9902937BE350089C8FB /* RJTHud.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RJTHud.m; sourceTree = ""; }; - 0BAAD9912937BE350089C8FB /* RJTHud.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RJTHud.h; sourceTree = ""; }; 0BAAD9952937BE840089C8FB /* Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Bridging-Header.h"; path = "Twackup/Bridging-Header.h"; sourceTree = ""; }; 0BAAD99629388D310089C8FB /* DebsListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebsListVC.swift; sourceTree = ""; }; 0BAAD99B293893EF0089C8FB /* DpkgDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DpkgDataProvider.swift; sourceTree = ""; }; @@ -149,6 +146,9 @@ 0BE7803F294DC60C00AE77CE /* DpkgListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DpkgListModel.swift; sourceTree = ""; }; 0BE87E8F2C1E0565009C95A7 /* Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; 0BE87E942C1E2214009C95A7 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + 0BE87E962C1E23A2009C95A7 /* Hud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hud.swift; sourceTree = ""; }; + 0BE87E972C1E23A2009C95A7 /* HudCircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HudCircularProgressView.swift; sourceTree = ""; }; + 0BE87E9C2C1E23D3009C95A7 /* UIWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIWindow.swift; sourceTree = ""; }; 0BF07A4F2941027800EEC8FC /* SplitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitController.swift; sourceTree = ""; }; 0BF07A61294335F700EEC8FC /* FFILogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FFILogger.swift; sourceTree = ""; }; 0BF07A6329433BE100EEC8FC /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = ""; }; @@ -411,10 +411,9 @@ 0BAAD9942937BE3A0089C8FB /* HUD */ = { isa = PBXGroup; children = ( - 0BAAD9912937BE350089C8FB /* RJTHud.h */, - 0BAAD9902937BE350089C8FB /* RJTHud.m */, - 0BAAD98E2937BE350089C8FB /* RJTHudProgressView.h */, - 0BAAD98F2937BE350089C8FB /* RJTHudProgressView.m */, + 0BE87E9C2C1E23D3009C95A7 /* UIWindow.swift */, + 0BE87E962C1E23A2009C95A7 /* Hud.swift */, + 0BE87E972C1E23A2009C95A7 /* HudCircularProgressView.swift */, ); path = HUD; sourceTree = ""; @@ -627,15 +626,15 @@ 0B88C31E2930DEA200BA5312 /* PackageDetailView.swift in Sources */, 0B85ABB7294C8E4C00B6C5CF /* DetailedLabelSUI.swift in Sources */, 0B86CDCD2C199FA500F3B4A0 /* Jailbreaks.swift in Sources */, - 0BAAD9922937BE360089C8FB /* RJTHudProgressView.m in Sources */, 0B88C3152930CE2E00BA5312 /* PackageListModel.swift in Sources */, 0B0DBB6F2934FA58002F3BB2 /* String.swift in Sources */, 0B50FF6E2949B80900CF4BCF /* CapacityBarLabel.swift in Sources */, 0B5B328C2943827500B51274 /* PackagesRebuilder.swift in Sources */, 0B2636F02948EC0A003EA806 /* CapacityBarView.swift in Sources */, 0B5D3F972934CB2C00AE50F2 /* PackageDataProvider.swift in Sources */, + 0BE87E982C1E23A2009C95A7 /* Hud.swift in Sources */, + 0BE87E992C1E23A2009C95A7 /* HudCircularProgressView.swift in Sources */, 0B86616D2930ADA300EA4301 /* MainTabbarController.swift in Sources */, - 0BAAD9932937BE360089C8FB /* RJTHud.m in Sources */, 0B50FF702949B82900CF4BCF /* CapacityView.swift in Sources */, 0B6BB2F1294A599D001E3B43 /* ProxiedObservedObject.swift in Sources */, 0B2636E62948B92E003EA806 /* SettingsViewController.swift in Sources */, @@ -662,6 +661,7 @@ 0B86CDF92C19AC4E00F3B4A0 /* dyn.c in Sources */, 0B0DBB72293501DE002F3BB2 /* Package.swift in Sources */, 0B86616B2930ADA300EA4301 /* SceneDelegate.swift in Sources */, + 0BE87E9D2C1E23D3009C95A7 /* UIWindow.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/twackup-gui/Twackup/Bridging-Header.h b/twackup-gui/Twackup/Bridging-Header.h index 1ad3def..ee88a39 100644 --- a/twackup-gui/Twackup/Bridging-Header.h +++ b/twackup-gui/Twackup/Bridging-Header.h @@ -9,8 +9,8 @@ #define Twackup_Bridging_Header_h #import "twackup.h" -#import "RJTHUD.h" #import "libroot.h" +#import @interface UIScrollView (Private) @property (assign, nonatomic, readonly, getter=_minimumContentOffset) CGPoint minimumContentOffset; diff --git a/twackup-gui/Twackup/Sources/ViewControllers/Package/Detail/DpkgDetailVC.swift b/twackup-gui/Twackup/Sources/ViewControllers/Package/Detail/DpkgDetailVC.swift index 7f06a10..04a682b 100644 --- a/twackup-gui/Twackup/Sources/ViewControllers/Package/Detail/DpkgDetailVC.swift +++ b/twackup-gui/Twackup/Sources/ViewControllers/Package/Detail/DpkgDetailVC.swift @@ -12,17 +12,13 @@ class DpkgDetailVC: PackageDetailVC, RebuildPackageDetailedViewDelegate { override var detailView: PackageDetailedView { _container } nonisolated func rebuild(_ package: FFIPackage) { - Task(priority: .userInitiated) { - let hud = await RJTHud.show() + Task { + let hud = await Hud.show() - Task(priority: .utility) { - let rebuilder = PackagesRebuilder(mainModel: mainModel) - await rebuilder.rebuild(packages: [package]) + let rebuilder = PackagesRebuilder(mainModel: mainModel) + await rebuilder.rebuild(packages: [package]) - Task(priority: .userInitiated) { - await hud?.hide(animated: true) - } - } + await hud?.hide(animated: true) } } } diff --git a/twackup-gui/Twackup/Sources/ViewControllers/Package/List/DpkgListVC.swift b/twackup-gui/Twackup/Sources/ViewControllers/Package/List/DpkgListVC.swift index fc53a30..e04b847 100644 --- a/twackup-gui/Twackup/Sources/ViewControllers/Package/List/DpkgListVC.swift +++ b/twackup-gui/Twackup/Sources/ViewControllers/Package/List/DpkgListVC.swift @@ -8,7 +8,7 @@ class DpkgListVC: SelectablePackageListVC { let dpkgModel: DpkgListModel - private var rebuildHUD: RJTHud? + private var rebuildHUD: Hud? private lazy var rebuildAllBarBtn: UIBarButtonItem = { let title = "debs-rebuildall-btn".localized @@ -99,9 +99,9 @@ class DpkgListVC: SelectablePackageListVC { func rebuild(packages: [Package]) { guard !packages.isEmpty else { return } - rebuildHUD = RJTHud.show() + rebuildHUD = Hud.show() rebuildHUD?.text = "rebuild-packages-status-title".localized - rebuildHUD?.style = .spinner + rebuildHUD?.style = .arcRotate Task(priority: .medium) { let rebuilder = PackagesRebuilder(mainModel: model.mainModel) { progress in @@ -116,7 +116,7 @@ class DpkgListVC: SelectablePackageListVC { } func updateProgress(_ progress: Progress) { - let hud = RJTHud.show() + let hud = Hud.show() hud?.detailedText = String( format: "rebuild-packages-status".localized, progress.completedUnitCount, diff --git a/twackup-gui/Twackup/Sources/Views/HUD/Hud.swift b/twackup-gui/Twackup/Sources/Views/HUD/Hud.swift new file mode 100644 index 0000000..7d2ac64 --- /dev/null +++ b/twackup-gui/Twackup/Sources/Views/HUD/Hud.swift @@ -0,0 +1,248 @@ +// +// HudCircularProgressView.swift +// iAppsDRM +// +// Created by Daniil on 10.01.2024. +// + +import UIKit + +final class Hud: UIView { + enum Style { + case arc, arcRotate, textOnly + } + + var progress: CGFloat { + get { + progressView.progress + } + set { + setProgress(newValue, animated: false) + } + } + + var style: Style = .arcRotate { + didSet { + updateProgressView() + } + } + + var text: String? { + didSet { + setText(text, for: textLabel) + } + } + + var detailedText: String? { + didSet { + setText(detailedText, for: detailedTextLabel) + } + } + + var blurStyle: UIBlurEffect.Style = .systemThickMaterial { + didSet { + blurView.effect = UIBlurEffect(style: blurStyle) + } + } + + var cornerRadius: CGFloat = 24.0 { + didSet { + blurView.layer.cornerRadius = cornerRadius + } + } + + private lazy var blurView: UIVisualEffectView = { + let view = UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.masksToBounds = true + view.layer.cornerRadius = cornerRadius + view.layer.cornerCurve = .continuous + view.backgroundColor = .init(white: 1.0, alpha: 0.3) + + view.contentView.layoutMargins = UIEdgeInsets(top: 24.0, left: 24.0, bottom: 24.0, right: 24.0) + + return view + }() + + private(set) lazy var progressView: HudCircularProgressView = { + HudCircularProgressView(frame: CGRect(x: 0.0, y: 0.0, width: 66.0, height: 66.0)) + }() + + private lazy var textLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 1 + label.textAlignment = .center + label.textColor = .label + label.font = UIFont.boldSystemFont(ofSize: UIFont.buttonFontSize) + + return label + }() + + private lazy var detailedTextLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.textAlignment = .center + label.textColor = .secondaryLabel + label.font = UIFont.boldSystemFont(ofSize: UIFont.systemFontSize) + + return label + }() + + private lazy var stackView: UIStackView = { + let view = UIStackView(arrangedSubviews: [progressView, textLabel, detailedTextLabel]) + view.translatesAutoresizingMaskIntoConstraints = false + view.axis = .vertical + view.alignment = .center + view.spacing = 10.0 + view.setCustomSpacing(24.0, after: progressView) + + return view + }() + + override init(frame: CGRect) { + super.init(frame: .zero) + + tintColor = .systemGray + + layer.shadowOpacity = 0.2 + layer.shadowRadius = 10.0 + layer.shadowOffset = CGSize(width: 0.0, height: 10.0) + layer.shadowColor = UIColor.black.cgColor + + let blurContent = blurView.contentView + let blurContentMargins = blurContent.layoutMarginsGuide + + blurContent.addSubview(stackView) + addSubview(blurView) + + NSLayoutConstraint.activate([ + blurView.topAnchor.constraint(equalTo: topAnchor), + blurView.bottomAnchor.constraint(equalTo: bottomAnchor), + blurView.leadingAnchor.constraint(equalTo: leadingAnchor), + blurView.trailingAnchor.constraint(equalTo: trailingAnchor), + + stackView.topAnchor.constraint(equalTo: blurContentMargins.topAnchor), + stackView.bottomAnchor.constraint(equalTo: blurContentMargins.bottomAnchor), + stackView.leadingAnchor.constraint(equalTo: blurContentMargins.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: blurContentMargins.trailingAnchor) + ]) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + static func show() -> Self? { + guard + let scene = UIWindow.focusedScene, + let keyWindow = scene.windows.first(where: { $0.isKeyWindow }) + else { return nil } + + let hud = Self() + hud.show(on: keyWindow, animated: true) + + return hud + } + + override func layoutSubviews() { + super.layoutSubviews() + + layer.shadowPath = CGPath( + roundedRect: bounds, + cornerWidth: cornerRadius, + cornerHeight: cornerRadius, + transform: nil + ) + } + + override func tintColorDidChange() { + super.tintColorDidChange() + + progressView.tintColor = tintColor + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + + if let superview { + translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + centerXAnchor.constraint(equalTo: superview.centerXAnchor), + centerYAnchor.constraint(equalTo: superview.centerYAnchor) + ]) + + updateProgressView() + } + } + + // MARK: - Public methods + + func show(on view: UIView, animated: Bool = true) { + if animated { + alpha = 0.0 + view.addSubview(self) + + UIView.animate(withDuration: 0.25) { [self] in + alpha = 1.0 + } + } else { + view.addSubview(self) + } + } + + func hide(animated: Bool = true, delaySec: TimeInterval? = nil) async { + if let delaySec { + try? await Task.sleep(nanoseconds: UInt64(delaySec) * 1_000_000_000) + } + + if animated { + UIView.animate(withDuration: 0.5) { [self] in + alpha = 0.0 + } completion: { [self] _ in + progressView.stopAnimation() + removeFromSuperview() + } + } else { + progressView.stopAnimation() + removeFromSuperview() + } + } + + func setProgress(_ progress: CGFloat, animated: Bool) { + progressView.setProgress(progress, animated: animated) + } + + // MARK: - Private methods + + private func updateProgressView() { + let isAlreadyAnimating = progressView.isAnimating + + switch style { + case .arc: + progressView.animationStyle = .arc + progressView.runAnimation() + + case .arcRotate: + progressView.animationStyle = .arcRotate + progressView.runAnimation() + + default: + progressView.stopAnimation() + } + + if isAlreadyAnimating != progressView.isAnimating { + progressView.invalidateIntrinsicContentSize() + } + } + + private func setText(_ text: String?, for label: UILabel) { + let textTransition = CATransition() + textTransition.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + textTransition.type = .fade + textTransition.duration = 0.5 + label.layer.add(textTransition, forKey: "textChangeAnimation") + label.text = text + } +} diff --git a/twackup-gui/Twackup/Sources/Views/HUD/HudCircularProgressView.swift b/twackup-gui/Twackup/Sources/Views/HUD/HudCircularProgressView.swift new file mode 100644 index 0000000..47fdaee --- /dev/null +++ b/twackup-gui/Twackup/Sources/Views/HUD/HudCircularProgressView.swift @@ -0,0 +1,182 @@ +// +// HudCircularProgressView.swift +// iAppsDRM +// +// Created by Daniil on 10.01.2024. +// + +import UIKit + +final class HudCircularProgressView: UIView { + enum AnimationStyle { + case arc, arcRotate + } + + override final class var layerClass: AnyClass { + CAShapeLayer.self + } + + var animationStyle: AnimationStyle = .arc { + didSet { + runAnimation() + } + } + + private(set) var progress: CGFloat = 0.75 + + private(set) var isAnimating = false + + private let arcSize: CGSize + + override var layer: CAShapeLayer { + super.layer as! CAShapeLayer + } + + override var intrinsicContentSize: CGSize { + isAnimating ? arcSize : .zero + } + + override init(frame: CGRect) { + let diameter = min(frame.height, frame.width) + let center = CGPoint(x: frame.width / 2, y: frame.height / 2) + arcSize = CGSize(width: diameter, height: diameter) + + super.init(frame: frame) + + layer.fillColor = UIColor.clear.cgColor + layer.lineCap = .round + layer.lineWidth = 4.0 + layer.strokeEnd = 0.5 + + let arcPath = CGMutablePath() + arcPath.addArc( + center: center, + radius: (diameter - layer.lineWidth) / 2, + startAngle: 0.0, + endAngle: 2.0 * .pi, + clockwise: false + ) + layer.path = arcPath + + let notificationCenter = NotificationCenter.default + notificationCenter.addObserver( + self, + selector: #selector(applicationDidBecomeActive), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + notificationCenter.addObserver( + self, + selector: #selector(applicationDidEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func tintColorDidChange() { + super.tintColorDidChange() + + layer.strokeColor = tintColor.cgColor + } + + private func runArcAnimation() { + let animation = CABasicAnimation(keyPath: "transform.rotation.z") + animation.fromValue = 0.0 + animation.toValue = Double.pi * 2.0 + animation.duration = 1.75 + animation.isCumulative = true + animation.repeatCount = .greatestFiniteMagnitude + + layer.add(animation, forKey: "rotate") + } + + private func runArcDotAnimation() { + runArcAnimation() + + let strokeDuration: CFTimeInterval = 2.25 + + let startAnimation = CABasicAnimation(keyPath: "strokeStart") + startAnimation.beginTime = strokeDuration * 0.15 + startAnimation.fromValue = 0.0 + startAnimation.toValue = 0.85 + startAnimation.duration = strokeDuration + startAnimation.timingFunction = CAMediaTimingFunction(controlPoints: 0.2, 0.88, 0.09, 0.99) + startAnimation.fillMode = .backwards + + let endAnimation = CABasicAnimation(keyPath: "strokeEnd") + endAnimation.fromValue = 0.0 + endAnimation.toValue = 1.075 + endAnimation.duration = strokeDuration + endAnimation.timingFunction = CAMediaTimingFunction(controlPoints: 0.4, 0.88, 0.09, 0.99) + endAnimation.fillMode = .forwards + + let strokeAnimation = CAAnimationGroup() + strokeAnimation.animations = [endAnimation, startAnimation] + strokeAnimation.duration = strokeDuration + strokeAnimation.repeatCount = .greatestFiniteMagnitude + + layer.add(strokeAnimation, forKey: "strokeFill") + } + + // MARK: - Public methods + + func runAnimation() { + stopAnimation() + isAnimating = true + + setProgress(progress, animated: false) + + switch animationStyle { + case .arc: + runArcAnimation() + + case .arcRotate: + runArcDotAnimation() + } + } + + func stopAnimation() { + isAnimating = false + + layer.removeAllAnimations() + layer.strokeEnd = 0.0 + } + + func setProgress(_ progress: CGFloat, animated: Bool) { + self.progress = progress + + if animated { + let animation = CABasicAnimation(keyPath: "strokeEnd") + animation.fromValue = layer.strokeEnd + animation.toValue = progress + animation.duration = 1.0 + + layer.add(animation, forKey: "strokeEnd") + } + + layer.strokeEnd = progress + } + + // MARK: - Private methods + + @objc + private func applicationDidBecomeActive() { + if isAnimating { + runAnimation() + } + } + + @objc + private func applicationDidEnterBackground() { + layer.removeAllAnimations() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } +} diff --git a/twackup-gui/Twackup/Sources/Views/HUD/RJTHud.h b/twackup-gui/Twackup/Sources/Views/HUD/RJTHud.h deleted file mode 100644 index 50a2167..0000000 --- a/twackup-gui/Twackup/Sources/Views/HUD/RJTHud.h +++ /dev/null @@ -1,109 +0,0 @@ -// -// RJTHud.h -// RJTranslate -// -// Created by Даниил on 28/10/2018. -// Copyright © 2018 Даниил. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -#if defined(__IPHONE_14_0) || defined(__MAC_10_16) || defined(__TVOS_14_0) || defined(__WATCHOS_7_0) -#define RJTHUD_OBJC_DIRECT_MEMBERS __attribute__((objc_direct_members)) -#define RJTHUD_OBJC_DIRECT __attribute__((objc_direct)) -#define RJTHUD_DIRECT ,direct -#else -#define RJTHUD_OBJC_DIRECT_MEMBERS -#define RJTHUD_OBJC_DIRECT -#define RJTHUD_DIRECT -#endif - -/** - Определяет стиль индикатора. - - - RJTHudStyleSpinner: Показывает спиннер и текст (если есть). - - RJTHudStyleTextOnly: Показывает только текст. - */ -typedef NS_ENUM(NSInteger, RJTHudStyle) { - RJTHudStyleProgress = 0, - RJTHudStyleSpinner, - RJTHudStyleTextOnly -}; - - -@interface RJTHud : UIView - -/** - Выполняет показ индикатора на видимом окне UIWindow. - - @return Возвращает экземпляр индикатора для дальнейшей настройки. - */ -+ (nullable instancetype)show RJTHUD_OBJC_DIRECT; - - -/** - @brief Устанавливает прогресс спиннера - - @discussion По умолчанию, установлен на 0.75. - */ -@property (assign, nonatomic RJTHUD_DIRECT) CGFloat progress; - -/** - Устанавливает стиль индикатора. - */ -@property (readwrite, assign, nonatomic RJTHUD_DIRECT) RJTHudStyle style; - -/** - Устанавливает основной текст в индикатор. - Для скрытия должен быть установлен в nil. - */ -@property (readwrite, copy, nonatomic, nullable RJTHUD_DIRECT) NSString *text; - -/** - Устанавливает дополнительный текст в индикатор. - Для скрытия должен быть установлен в nil. - */ -@property (readwrite, copy, nonatomic, nullable RJTHUD_DIRECT) NSString *detailedText; - -/** - Стиль заднего размытия. По умолчанию, UIBlurEffectStyleSystemThickMaterial - */ -@property (assign, nonatomic RJTHUD_DIRECT) UIBlurEffectStyle blurStyle; - -/** - Устанавливает прогресс спиннера. - - @param progress Значение прогресса от 0 до 1 - @param animated Если задан YES, то прогресс будет установлен с анимацией. В противном случае - без. - */ -- (void)setProgress:(CGFloat)progress animated:(BOOL)animated RJTHUD_OBJC_DIRECT; - -/** - Выполняет показ индикатора на окне. - - @param animated Если флаг установлен в YES, выполняется с анимацией. В противном случае - без. - @param view Представление, на котором необходимо показать индикатор. - */ -- (void)showAnimated:(BOOL)animated onView:(UIView *)view RJTHUD_OBJC_DIRECT; - -/** - Выполняет скрытие индикатора. - - @param animated Если флаг установлен в YES, выполняется с анимацией. В противном случае - без. - */ -- (void)hideAnimated:(BOOL)animated RJTHUD_OBJC_DIRECT; - -/** - Выполняет анимированное скрытие индикатора с задержкой. - - @param delay Время перед скрытием индикатора. - */ -- (void)hideAfterDelay:(CGFloat)delay RJTHUD_OBJC_DIRECT; - -- (void)performOnMainThread:(void(^)(void))block RJTHUD_OBJC_DIRECT; - -@end - -NS_ASSUME_NONNULL_END diff --git a/twackup-gui/Twackup/Sources/Views/HUD/RJTHud.m b/twackup-gui/Twackup/Sources/Views/HUD/RJTHud.m deleted file mode 100644 index 5304df8..0000000 --- a/twackup-gui/Twackup/Sources/Views/HUD/RJTHud.m +++ /dev/null @@ -1,395 +0,0 @@ -// -// RJTHud.m -// RJTranslate -// -// Created by Даниил on 28/10/2018. -// Copyright © 2018 Даниил. All rights reserved. -// - -#import "RJTHud.h" -#import "RJTHudProgressView.h" - -RJTHUD_OBJC_DIRECT_MEMBERS -@interface RJTHud () -@property (strong, nonatomic RJTHUD_DIRECT) UIView *contentView; -@property (strong, nonatomic RJTHUD_DIRECT) UIVisualEffectView *blurView; - -@property (strong, nonatomic RJTHUD_DIRECT) RJTHudProgressView *progressView; -@property (strong, nonatomic RJTHUD_DIRECT) UILabel *textLabel; -@property (strong, nonatomic RJTHUD_DIRECT) UILabel *detailedTextLabel; - -@property (strong, nonatomic RJTHUD_DIRECT) NSLayoutConstraint *widthConstraint; -@property (strong, nonatomic RJTHUD_DIRECT) NSLayoutConstraint *heightConstraint; - -@property (strong, nonatomic RJTHUD_DIRECT) NSLayoutConstraint *textLabelHeightConstraint; -@property (strong, nonatomic RJTHUD_DIRECT) NSLayoutConstraint *detailedTextLabelHeightConstraint; - -@property (assign, nonatomic, readonly RJTHUD_DIRECT) CGFloat labelsDefaultHeight; - -- (void)runSpinner RJTHUD_OBJC_DIRECT; - -@end - -RJTHUD_OBJC_DIRECT_MEMBERS -@implementation RJTHud - -static CGFloat const RJTHudContentSize = 140.0; - -+ (nullable instancetype)show { - for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) { - if (scene.activationState != UISceneActivationStateForegroundActive) { - continue; - } - - if (![scene isKindOfClass:[UIWindowScene class]]) { - continue; - } - - for (UIWindow *window in ((UIWindowScene *)scene).windows) { - if (window.keyWindow) { - RJTHud *hud = [RJTHud new]; - [hud showAnimated:YES onView:window]; - hud.progress = 0.75; - - return hud; - } - } - } - - return nil; -} - -- (instancetype)init { - self = [super initWithFrame:CGRectZero]; - if (self) { - _labelsDefaultHeight = 32.0; - _blurStyle = UIBlurEffectStyleSystemThickMaterial; - self.translatesAutoresizingMaskIntoConstraints = NO; - - [self createViewHierarchy]; - } - return self; -} - -#pragma mark - Views - -- (UIView *)contentView { - if (!_contentView) { - _contentView = [UIView new]; - _contentView.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.3]; - _contentView.layer.cornerRadius = 24.0; - _contentView.layer.cornerCurve = kCACornerCurveContinuous; - - _contentView.layer.shadowOpacity = 0.2f; - _contentView.layer.shadowColor = UIColor.blackColor.CGColor; - _contentView.layer.shadowRadius = 10.0; - _contentView.layer.shadowOffset = CGSizeMake(0.0, 10.0); - } - - return _contentView; -} - -- (UIVisualEffectView *)blurView { - if (!_blurView) { - _blurView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:self.blurStyle]]; - _blurView.layer.masksToBounds = YES; - _blurView.layer.cornerRadius = self.contentView.layer.cornerRadius; - _blurView.layer.cornerCurve = kCACornerCurveContinuous; - } - - return _blurView; -} - -- (RJTHudProgressView *)progressView { - if (!_progressView) { - _progressView = RJTHudProgressView.defaultProgressView; - _progressView.tintColor = UIColor.systemGrayColor; - } - - return _progressView; -} - -- (UILabel *)textLabel { - if (!_textLabel) { - _textLabel = [[UILabel alloc] init]; - _textLabel.numberOfLines = 1; - _textLabel.textAlignment = NSTextAlignmentCenter; - _textLabel.textColor = UIColor.labelColor; - _textLabel.font = [UIFont boldSystemFontOfSize:[UIFont buttonFontSize]]; - } - - return _textLabel; -} - -- (UILabel *)detailedTextLabel { - if (!_detailedTextLabel) { - _detailedTextLabel = [[UILabel alloc] init]; - _detailedTextLabel.numberOfLines = 0; - _detailedTextLabel.textAlignment = NSTextAlignmentCenter; - _detailedTextLabel.textColor = [UIColor secondaryLabelColor]; - _detailedTextLabel.font = [UIFont boldSystemFontOfSize:[UIFont systemFontSize]]; - } - - return _detailedTextLabel; -} - -- (UIStackView *)stackView { - UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[ - self.progressView, self.textLabel, self.detailedTextLabel - ]]; - stackView.axis = UILayoutConstraintAxisVertical; - stackView.distribution = UIStackViewDistributionFill; - stackView.alignment = UIStackViewAlignmentCenter; - - return stackView; -} - -#pragma mark - -#pragma mark Public -#pragma mark - - -- (void)showAnimated:(BOOL)animated onView:(UIView *)view { - [self performOnMainThread:^{ - if (animated) { - self.alpha = 0.0; - [view addSubview:self]; - - [self animateWithDuration:0.25 animations:^{ - self.alpha = 1.0; - } completion:nil]; - } else { - [view addSubview:self]; - } - - [self runSpinner]; - }]; -} - -- (void)hideAnimated:(BOOL)animated { - [self performOnMainThread:^{ - if (animated) { - [self animateWithDuration:0.5 animations:^{ - self.alpha = 0.0; - } completion:^(BOOL finished) { - [self.progressView stopAnimating]; - [self removeFromSuperview]; - }]; - } else { - [self.progressView stopAnimating]; - [self removeFromSuperview]; - } - }]; -} - -- (void)hideAfterDelay:(CGFloat)delay { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self hideAnimated:YES]; - }); -} - -- (void)setProgress:(CGFloat)progress { - [self setProgress:progress animated:NO]; -} - -- (void)setProgress:(CGFloat)progress animated:(BOOL)animated { - _progress = progress; - - [self performOnMainThread:^{ - [self.progressView setProgress:progress animated:animated]; - }]; -} - - -#pragma mark - Private - - -- (void)createViewHierarchy { - UIView *contentView = self.contentView; - [self addSubview:contentView]; - - UIVisualEffectView *blurView = self.blurView; - [contentView addSubview:blurView]; - - UIStackView *stackView = self.stackView; - [blurView.contentView addSubview:stackView]; - - - stackView.translatesAutoresizingMaskIntoConstraints = NO; - blurView.translatesAutoresizingMaskIntoConstraints = NO; - contentView.translatesAutoresizingMaskIntoConstraints = NO; - - self.widthConstraint = [contentView.widthAnchor constraintEqualToConstant:RJTHudContentSize]; - self.heightConstraint = [contentView.heightAnchor constraintEqualToConstant:RJTHudContentSize]; - self.textLabelHeightConstraint = [self.textLabel.heightAnchor constraintEqualToConstant:0.0]; - self.detailedTextLabelHeightConstraint = [self.detailedTextLabel.heightAnchor constraintEqualToConstant:0.0]; - - [NSLayoutConstraint activateConstraints:@[ - [contentView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor], - [contentView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor], - - self.widthConstraint, self.heightConstraint, - self.textLabelHeightConstraint, self.detailedTextLabelHeightConstraint, - - [stackView.centerXAnchor constraintEqualToAnchor:blurView.contentView.centerXAnchor], - [stackView.centerYAnchor constraintEqualToAnchor:blurView.contentView.centerYAnchor], - - [blurView.topAnchor constraintEqualToAnchor:contentView.topAnchor], - [blurView.bottomAnchor constraintEqualToAnchor:contentView.bottomAnchor], - [blurView.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor], - [blurView.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor], - ]]; -} - -- (void)didMoveToSuperview { - [super didMoveToSuperview]; - - UIView *superview = self.superview; - if (!superview) { - return; - } - - [NSLayoutConstraint activateConstraints:@[ - [self.topAnchor constraintEqualToAnchor:superview.topAnchor], - [self.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor], - [self.leadingAnchor constraintEqualToAnchor:superview.leadingAnchor], - [self.trailingAnchor constraintEqualToAnchor:superview.trailingAnchor], - ]]; -} - -- (void)setText:(NSString *)text { - if (!text.length) { - text = nil; - } - - _text = text; - [self setText:text forLabel:self.textLabel]; -} - -- (void)setDetailedText:(NSString *)detailedText { - if (!detailedText.length) { - detailedText = nil; - } - - _detailedText = detailedText; - [self setText:detailedText forLabel:self.detailedTextLabel]; -} - -- (void)setBlurStyle:(UIBlurEffectStyle)blurStyle { - _blurStyle = blurStyle; - - [self performOnMainThread:^{ - self.blurView.effect = [UIBlurEffect effectWithStyle:blurStyle]; - }]; -} - -- (void)setStyle:(RJTHudStyle)style { - _style = style; - - [self performOnMainThread:^{ - if (self.style == RJTHudStyleProgress || self.style == RJTHudStyleSpinner) { - [self runSpinner]; - - self.progressView.heightConstraint.constant = self.progressView.cachedHeight; - [self updateHudSizeWithCompletion:^{ - [self animateWithDuration:0.2 animations:^{ - self.progressView.alpha = 1.0; - } completion:nil]; - }]; - } else { - [self animateWithDuration:0.2 animations:^{ - self.progressView.alpha = 0.0; - } completion:^(BOOL finished) { - [self.progressView stopAnimating]; - self.progressView.heightConstraint.constant = 0.0; - [self updateHudSizeWithCompletion:nil]; - }]; - } - }]; -} - - - -#pragma mark - - -- (void)setText:(NSString *)text forLabel:(UILabel *)label { - [self performOnMainThread:^{ - label.text = text; - [self updateHudSizeWithCompletion:nil]; - }]; -} - -- (void)updateHudSizeWithCompletion:(void(^)(void))completion { - CGSize textSize = [self sizeForTextInLabel:self.textLabel]; - CGSize detailTextSize = [self sizeForTextInLabel:self.detailedTextLabel]; - - CGFloat width = 32.0; - if (textSize.width > RJTHudContentSize || detailTextSize.width > RJTHudContentSize) { - width += MAX(textSize.width, detailTextSize.width); - } else { - width = RJTHudContentSize; - } - self.widthConstraint.constant = width; - - - CGFloat progressViewHeight = self.progressView.heightConstraint.constant; - CGFloat minLabelHeight = self.labelsDefaultHeight; - - CGFloat height = progressViewHeight + MAX(textSize.height, minLabelHeight) + MAX(detailTextSize.height, minLabelHeight); - if (height > RJTHudContentSize) - height += (CGFloat)32.0; - - self.heightConstraint.constant = height; - self.textLabelHeightConstraint.constant = textSize.height; - self.detailedTextLabelHeightConstraint.constant = detailTextSize.height; - - const CGFloat cornerRadius = self.contentView.layer.cornerRadius; - CGPathRef shadowPath = CGPathCreateWithRoundedRect(CGRectMake(0.0, 0.0, width, height), cornerRadius, cornerRadius, NULL); - self.contentView.layer.shadowPath = shadowPath; - CGPathRelease(shadowPath); - - [self animateWithDuration:0.3 animations:^{ - [self.contentView layoutIfNeeded]; - } completion:^(BOOL finished) { - if (completion) - completion(); - }]; -} - -- (void)animateWithDuration:(NSTimeInterval)duration animations:(void(^)(void))animations - completion:(void (^ __nullable)(BOOL finished))completion { - NSAssert([NSThread isMainThread], @"%s must be called only from the main thread!", __FUNCTION__); - [UIView animateWithDuration:duration delay:0.0 - options:UIViewAnimationOptionAllowAnimatedContent | UIViewAnimationOptionAllowUserInteraction - animations:animations completion:completion]; -} - -- (void)performOnMainThread:(void(^)(void))block { - [NSThread isMainThread] ? block() : dispatch_sync(dispatch_get_main_queue(), block); -} - -- (CGSize)sizeForTextInLabel:(UILabel *)label { - NSString *text = label.text; - NSUInteger linesCount = [text componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]].count + 1; - - CGSize boundingSize = CGSizeMake(CGRectGetWidth([UIScreen mainScreen].bounds) - 16.0, self.labelsDefaultHeight); - CGSize textSize = [text boundingRectWithSize:boundingSize - options:0 attributes:@{NSFontAttributeName:label.font} context:nil].size; - textSize.height *= (CGFloat)linesCount; - - return textSize; -} - -- (void)runSpinner { - switch (self.style) { - case RJTHudStyleSpinner: - [self.progressView runSpinnerAnimation]; - break; - case RJTHudStyleProgress: - [self.progressView runBasicAnimation]; - break; - default: - break; - } -} - -@end diff --git a/twackup-gui/Twackup/Sources/Views/HUD/RJTHudProgressView.h b/twackup-gui/Twackup/Sources/Views/HUD/RJTHudProgressView.h deleted file mode 100644 index 6d640be..0000000 --- a/twackup-gui/Twackup/Sources/Views/HUD/RJTHudProgressView.h +++ /dev/null @@ -1,42 +0,0 @@ -// -// RJTHudProgressView.h -// RJTranslate -// -// Created by Даниил on 28/10/2018. -// Copyright © 2018 Даниил. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -#if defined(__IPHONE_14_0) || defined(__MAC_10_16) || defined(__TVOS_14_0) || defined(__WATCHOS_7_0) -#define RJTHUD_OBJC_DIRECT_MEMBERS __attribute__((objc_direct_members)) -#define RJTHUD_OBJC_DIRECT __attribute__((objc_direct)) -#define RJTHUD_DIRECT ,direct -#else -#define RJTHUD_OBJC_DIRECT_MEMBERS -#define RJTHUD_OBJC_DIRECT -#define RJTHUD_DIRECT -#endif - -@interface RJTHudProgressView : UIView - -+ (instancetype)defaultProgressView RJTHUD_OBJC_DIRECT; - -@property (assign, nonatomic RJTHUD_DIRECT) CGFloat progress; - -@property (assign, nonatomic, readonly RJTHUD_DIRECT) BOOL animating; - -@property (strong, nonatomic, readonly RJTHUD_DIRECT) NSLayoutConstraint *heightConstraint; -@property (assign, nonatomic, readonly RJTHUD_DIRECT) CGFloat cachedHeight; - -- (void)setProgress:(CGFloat)progress animated:(BOOL)animated RJTHUD_OBJC_DIRECT; - -- (void)runBasicAnimation RJTHUD_OBJC_DIRECT; -- (void)runSpinnerAnimation RJTHUD_OBJC_DIRECT; -- (void)stopAnimating RJTHUD_OBJC_DIRECT; - -@end - -NS_ASSUME_NONNULL_END diff --git a/twackup-gui/Twackup/Sources/Views/HUD/RJTHudProgressView.m b/twackup-gui/Twackup/Sources/Views/HUD/RJTHudProgressView.m deleted file mode 100644 index 2b5c95e..0000000 --- a/twackup-gui/Twackup/Sources/Views/HUD/RJTHudProgressView.m +++ /dev/null @@ -1,157 +0,0 @@ -// -// RJTHudProgressView.m -// RJTranslate -// -// Created by Даниил on 28/10/2018. -// Copyright © 2018 Даниил. All rights reserved. -// - -#import "RJTHudProgressView.h" - -@interface RJTHudProgressView () -@property (nonatomic, readonly, strong) CAShapeLayer *layer; -@property (nonatomic, readonly, assign) BOOL basicAnimation; -@end - -@implementation RJTHudProgressView -@dynamic layer; - -+ (Class)layerClass { - return [CAShapeLayer class]; -} - -+ (instancetype)defaultProgressView { - return [[RJTHudProgressView alloc] initWithFrame:CGRectMake(0.0, 0.0, 66.0, 66.0)]; -} - -- (instancetype)initWithFrame:(CGRect)frame { - CGFloat size = (CGRectGetWidth(frame) + CGRectGetHeight(frame)) / 2.0; - frame.size = CGSizeMake(size, size); - - self = [super initWithFrame:frame]; - if (self) { - CGFloat center = size / 2.0; - - CGMutablePathRef mutablePath = CGPathCreateMutable(); - CGPathAddArc(mutablePath, NULL, center, center, center - 8.0, 0.0, (CGFloat)(2.0 * M_PI), NO); - - CAShapeLayer *layer = self.layer; - layer.path = mutablePath; - layer.fillColor = [UIColor clearColor].CGColor; - layer.lineCap = kCALineCapRound; - layer.lineWidth = 3.0; - layer.strokeEnd = 0.0; - - CGPathRelease(mutablePath); - - self.progress = 0.0; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil]; - } - return self; -} - -- (void)tintColorDidChange { - [super tintColorDidChange]; - - self.layer.strokeColor = self.tintColor.CGColor; -} - -- (void)runBasicAnimation { - [self.layer removeAllAnimations]; - - _animating = YES; - _basicAnimation = YES; - - CABasicAnimation *rotateAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; - rotateAnimation.fromValue = @0.0; - rotateAnimation.toValue = @(M_PI * 2.0); - rotateAnimation.duration = 1.75; - rotateAnimation.repeatCount = INFINITY; - [self.layer addAnimation:rotateAnimation forKey:@"rotate"]; -} - -- (void)runSpinnerAnimation { - [self.layer removeAllAnimations]; - - [self runBasicAnimation]; - _basicAnimation = NO; - - const CFTimeInterval strokeDuration = 2.25; - - CABasicAnimation *startAnim = [CABasicAnimation animationWithKeyPath:@"strokeStart"]; - startAnim.beginTime = strokeDuration * 0.15; - startAnim.fromValue = @0.0; - startAnim.toValue = @0.85; - startAnim.duration = strokeDuration; - startAnim.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.2f :0.88f :0.09f :0.99f]; - startAnim.fillMode = kCAFillModeBackwards; - - CABasicAnimation *endAnim = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; - endAnim.beginTime = 0.0; - endAnim.fromValue = @0.0; - endAnim.toValue = @1.075; - endAnim.duration = strokeDuration; - endAnim.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.4f :0.88f :0.09f :0.99f]; - endAnim.fillMode = kCAFillModeForwards; - - CAAnimationGroup *strokeAnim = [CAAnimationGroup animation]; - strokeAnim.animations = @[endAnim, startAnim]; - strokeAnim.duration = strokeDuration; - strokeAnim.repeatCount = INFINITY; - - [self.layer addAnimation:strokeAnim forKey:@"strokeFill"]; -} - -- (void)stopAnimating { - _animating = NO; - [self.layer removeAllAnimations]; -} - -- (void)setProgress:(CGFloat)progress { - [self setProgress:progress animated:NO]; -} - -- (void)setProgress:(CGFloat)progress animated:(BOOL)animated { - _progress = progress; - - if (animated) { - CABasicAnimation *strokeEndAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; - strokeEndAnimation.fromValue = @(self.layer.strokeEnd); - strokeEndAnimation.toValue = @(progress); - strokeEndAnimation.duration = 1.0; - [self.layer addAnimation:strokeEndAnimation forKey:@"strokeEnd"]; - } - - self.layer.strokeEnd = progress; -} - -- (void)didMoveToSuperview { - [super didMoveToSuperview]; - - _cachedHeight = CGRectGetHeight(self.frame); - _heightConstraint = [self.heightAnchor constraintEqualToConstant:self.cachedHeight]; - - [NSLayoutConstraint activateConstraints:@[ - _heightConstraint, [self.widthAnchor constraintEqualToConstant:CGRectGetWidth(self.frame)] - ]]; -} - -- (void)applicationDidBecomeActive { - if (self.animating && self.basicAnimation) { - [self runBasicAnimation]; - } else if (self.animating) { - [self runSpinnerAnimation]; - } -} - -- (void)applicationDidEnterBackground { - [self.layer removeAllAnimations]; -} - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -@end diff --git a/twackup-gui/Twackup/Sources/Views/HUD/UIWindow.swift b/twackup-gui/Twackup/Sources/Views/HUD/UIWindow.swift new file mode 100644 index 0000000..505d37d --- /dev/null +++ b/twackup-gui/Twackup/Sources/Views/HUD/UIWindow.swift @@ -0,0 +1,18 @@ +// +// UIWindow.swift +// iAppsDRM +// +// Created by Daniil on 08.01.2024. +// Copyright © 2024 Даниил. All rights reserved. +// + +import UIKit + +extension UIWindow { + static var focusedScene: UIWindowScene? { + UIApplication.shared.connectedScenes.first { scene in + let state = scene.activationState + return (state == .foregroundActive || state == .foregroundInactive) && scene is UIWindowScene + } as? UIWindowScene + } +}