diff --git a/TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIconBadge.imageset/Contents.json b/TwoFAS/TwoFAS/Other/Assets.xcassets/Badge.imageset/Contents.json similarity index 72% rename from TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIconBadge.imageset/Contents.json rename to TwoFAS/TwoFAS/Other/Assets.xcassets/Badge.imageset/Contents.json index cd45fd1c..b2f50124 100644 --- a/TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIconBadge.imageset/Contents.json +++ b/TwoFAS/TwoFAS/Other/Assets.xcassets/Badge.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "NavibarNewsIconBadge.pdf", + "filename" : "Ellipse 10.pdf", "idiom" : "universal" } ], diff --git a/TwoFAS/TwoFAS/Other/Assets.xcassets/Badge.imageset/Ellipse 10.pdf b/TwoFAS/TwoFAS/Other/Assets.xcassets/Badge.imageset/Ellipse 10.pdf new file mode 100644 index 00000000..758b5af2 --- /dev/null +++ b/TwoFAS/TwoFAS/Other/Assets.xcassets/Badge.imageset/Ellipse 10.pdf @@ -0,0 +1,71 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.929412 0.109804 0.141176 scn +3.000000 1.500000 m +3.000000 0.671573 2.328427 0.000000 1.500000 0.000000 c +0.671573 0.000000 0.000000 0.671573 0.000000 1.500000 c +0.000000 2.328427 0.671573 3.000000 1.500000 3.000000 c +2.328427 3.000000 3.000000 2.328427 3.000000 1.500000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 371 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 3.000000 3.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000461 00000 n +0000000483 00000 n +0000000654 00000 n +0000000728 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +787 +%%EOF \ No newline at end of file diff --git a/TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIcon.imageset/Contents.json b/TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIcon.imageset/Contents.json index 544c4545..c9cedbec 100644 --- a/TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIcon.imageset/Contents.json +++ b/TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIcon.imageset/Contents.json @@ -8,8 +8,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" } } diff --git a/TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIcon.imageset/NavibarNewsIcon.pdf b/TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIcon.imageset/NavibarNewsIcon.pdf index 7383b1ae..6ec1c8bb 100644 Binary files a/TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIcon.imageset/NavibarNewsIcon.pdf and b/TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIcon.imageset/NavibarNewsIcon.pdf differ diff --git a/TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIconBadge.imageset/NavibarNewsIconBadge.pdf b/TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIconBadge.imageset/NavibarNewsIconBadge.pdf deleted file mode 100644 index 56ebcd71..00000000 Binary files a/TwoFAS/TwoFAS/Other/Assets.xcassets/NavibarNewsIconBadge.imageset/NavibarNewsIconBadge.pdf and /dev/null differ diff --git a/TwoFAS/TwoFAS/Other/Generated/Assets.swift b/TwoFAS/TwoFAS/Other/Generated/Assets.swift index daac0188..867372bf 100644 --- a/TwoFAS/TwoFAS/Other/Generated/Assets.swift +++ b/TwoFAS/TwoFAS/Other/Generated/Assets.swift @@ -34,6 +34,7 @@ internal enum Asset { internal static let deleteSettingsIcon = ImageAsset(name: "DeleteSettingsIcon") internal static let backupDeleted = ImageAsset(name: "backupDeleted") internal static let backupSettingsIcon = ImageAsset(name: "backupSettingsIcon") + internal static let badge = ImageAsset(name: "Badge") internal static let barsBackground = ImageAsset(name: "BarsBackground") internal static let bracket = ImageAsset(name: "Bracket") internal static let aboutExtension = ImageAsset(name: "AboutExtension") @@ -110,7 +111,6 @@ internal enum Asset { internal static let naviIconAddFirst = ImageAsset(name: "NaviIconAddFirst") internal static let naviSortIcon = ImageAsset(name: "NaviSortIcon") internal static let navibarNewsIcon = ImageAsset(name: "NavibarNewsIcon") - internal static let navibarNewsIconBadge = ImageAsset(name: "NavibarNewsIconBadge") internal static let notificationFeatures = ImageAsset(name: "NotificationFeatures") internal static let notificationNews = ImageAsset(name: "NotificationNews") internal static let notificationTips = ImageAsset(name: "NotificationTips") diff --git a/TwoFAS/TwoFAS/Root/Modules/News/Presenter/NewsPresenter.swift b/TwoFAS/TwoFAS/Root/Modules/News/Presenter/NewsPresenter.swift index 16af6a36..cff3f054 100644 --- a/TwoFAS/TwoFAS/Root/Modules/News/Presenter/NewsPresenter.swift +++ b/TwoFAS/TwoFAS/Root/Modules/News/Presenter/NewsPresenter.swift @@ -91,7 +91,8 @@ private extension NewsPresenter { func reload() { let now = Date() interactor.fetchList { [weak self] news in - let cells = news.map { entry in + let sortedNews = news.sorted { $0.publishedAt > $1.publishedAt } + var cells = sortedNews.map { entry in NewsCell( icon: entry.icon.image, title: entry.message ?? entry.link?.absoluteString ?? "", diff --git a/TwoFAS/TwoFAS/Root/Modules/Tokens/Presenter/TokensPresenter.swift b/TwoFAS/TwoFAS/Root/Modules/Tokens/Presenter/TokensPresenter.swift index 92a7489d..6086eede 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Tokens/Presenter/TokensPresenter.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Tokens/Presenter/TokensPresenter.swift @@ -531,7 +531,7 @@ private extension TokensPresenter { }() if interactor.hasServices { - updateAddServiceIcon() + updateNaviIcons() view?.showList() if Set(currentServices) != Set(newServices) || changeRequriesTokenRefresh { @@ -552,7 +552,7 @@ private extension TokensPresenter { if !isSearching && currentState == .edit { setCurrentState(.normal) } - updateAddServiceIcon() + updateNaviIcons() interactor.stopCounters() updateEditStateButton() @@ -595,17 +595,23 @@ private extension TokensPresenter { } func updateNewsIcon() { - updateAddServiceIcon() + updateNaviIcons() interactor.fetchNews { [weak self] in - self?.updateAddServiceIcon() + self?.updateNaviIcons(hasUnreadNews: self?.hasUnreadNews ?? false) } } - private func updateAddServiceIcon() { + private func updateNaviIcons(hasUnreadNews: Bool = false) { if interactor.hasServices { - view?.updateAddIcon(using: mapButtonStateFor(currentState, isFirst: false)) + view?.updateNaviIcons( + using: mapButtonStateFor(currentState, isFirst: false), + hasUnreadNews: hasUnreadNews + ) } else { - view?.updateAddIcon(using: mapButtonStateFor(currentState, isFirst: !isSearching)) + view?.updateNaviIcons( + using: mapButtonStateFor(currentState, isFirst: !isSearching), + hasUnreadNews: hasUnreadNews + ) } } } diff --git a/TwoFAS/TwoFAS/Root/Modules/Tokens/View/TokensViewController+TokensViewControlling.swift b/TwoFAS/TwoFAS/Root/Modules/Tokens/View/TokensViewController+TokensViewControlling.swift index 4b174899..78df38ae 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Tokens/View/TokensViewController+TokensViewControlling.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Tokens/View/TokensViewController+TokensViewControlling.swift @@ -29,7 +29,7 @@ protocol TokensViewControlling: AnyObject { func enableDragging() func disableDragging() - func updateAddIcon(using state: TokensViewControllerAddState) + func updateNaviIcons(using state: TokensViewControllerAddState, hasUnreadNews: Bool) func updateEditState(using state: TokensViewControllerEditState) func lockBars() @@ -125,22 +125,25 @@ extension TokensViewController: TokensViewControlling { func disableDragging() { tokensView.dragInteractionEnabled = false } - + // MARK: - Navibar icons - func updateAddIcon(using state: TokensViewControllerAddState) { - func createNewsIcon() -> UIBarButtonItem { - let img: UIImage = { - presenter.hasUnreadNews ? Asset.navibarNewsIconBadge.image : Asset.navibarNewsIcon.image - }() - img.withTintColor(Theme.Colors.Icon.theme) - let button = UIBarButtonItem( - image: img, - style: .plain, - target: self, - action: #selector(showNotifications) - ) - button.accessibilityLabel = T.Commons.notifications - return button + func updateNaviIcons(using state: TokensViewControllerAddState, hasUnreadNews: Bool) { + func createNewsButton() -> UIBarButtonItem { + if presenter.hasUnreadNews { + let naviButton = UnreadNewsNaviButton() + naviButton.translatesAutoresizingMaskIntoConstraints = false + naviButton.accessibilityLabel = T.Commons.notifications + naviButton.addTarget(self, action: #selector(showNotifications), for: .touchUpInside) + naviButton.animate() + return UIBarButtonItem(customView: naviButton) + } else { + let naviButton = UIButton(type: .custom) + naviButton.setBackgroundImage(Asset.navibarNewsIcon.image, for: .normal) + naviButton.addTarget(self, action: #selector(showNotifications), for: .touchUpInside) + naviButton.translatesAutoresizingMaskIntoConstraints = false + naviButton.accessibilityLabel = T.Commons.notifications + return UIBarButtonItem(customView: naviButton) + } } func createAddButton(image: UIImage) -> UIBarButtonItem { @@ -156,15 +159,33 @@ extension TokensViewController: TokensViewControlling { switch state { case .firstTime: - navigationItem.rightBarButtonItems = [ - createAddButton(image: Asset.naviIconAddFirst.image), - createNewsIcon() - ] + if let newsButton, hasUnreadNews { + navigationItem.rightBarButtonItems = [ + createAddButton(image: Asset.naviIconAddFirst.image), + newsButton + ] + } else { + let newsButton = createNewsButton() + self.newsButton = newsButton + navigationItem.rightBarButtonItems = [ + createAddButton(image: Asset.naviIconAddFirst.image), + newsButton + ] + } case .normal: - navigationItem.rightBarButtonItems = [ - createAddButton(image: Asset.naviIconAdd.image), - createNewsIcon() - ] + if let newsButton, !hasUnreadNews { + navigationItem.rightBarButtonItems = [ + createAddButton(image: Asset.naviIconAdd.image), + newsButton + ] + } else { + let newsButton = createNewsButton() + self.newsButton = newsButton + navigationItem.rightBarButtonItems = [ + createAddButton(image: Asset.naviIconAdd.image), + newsButton + ] + } case .none: let buttonSection = UIBarButtonItem( image: Asset.addCategory.image, @@ -354,3 +375,88 @@ extension TokensViewController { presenter.handleAppUnlocked() } } + +private extension TokensViewController { + final class UnreadNewsNaviButton: UIButton { + let newsImageView = UIImageView(image: Asset.navibarNewsIcon.image) + let badgeImageView = UIImageView(image: Asset.badge.image) + + private let badgeWidth: CGFloat = 3 + + override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupViews() + } + + private func setupViews() { + newsImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(newsImageView) + + badgeImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(badgeImageView) + badgeImageView.isHidden = true + + NSLayoutConstraint.activate([ + newsImageView.centerXAnchor.constraint(equalTo: centerXAnchor), + newsImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + badgeImageView.topAnchor.constraint(equalTo: topAnchor, constant: Theme.Metrics.halfSpacing), + badgeImageView.trailingAnchor.constraint( + equalTo: trailingAnchor, + constant: -Theme.Metrics.quaterSpacing + ), + badgeImageView.widthAnchor.constraint(equalToConstant: badgeWidth), + badgeImageView.heightAnchor.constraint(equalToConstant: badgeWidth) + ]) + } + + func animate() { + let angle: Double = .pi / 12 + let numberOfFrames: Double = 5 + let frameDuration = Double(0.7 / numberOfFrames) + + UIView.animateKeyframes( + withDuration: 1, + delay: 0, + animations: { [newsImageView] in + UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: frameDuration) { + newsImageView.transform = CGAffineTransform(rotationAngle: -angle) + } + UIView.addKeyframe(withRelativeStartTime: frameDuration, relativeDuration: frameDuration) { + newsImageView.transform = CGAffineTransform(rotationAngle: +angle) + } + UIView.addKeyframe(withRelativeStartTime: 2 * frameDuration, relativeDuration: frameDuration) { + newsImageView.transform = CGAffineTransform(rotationAngle: -angle) + } + UIView.addKeyframe(withRelativeStartTime: 3 * frameDuration, relativeDuration: frameDuration) { + newsImageView.transform = CGAffineTransform(rotationAngle: +angle) + } + UIView.addKeyframe(withRelativeStartTime: 4 * frameDuration, relativeDuration: frameDuration) { + newsImageView.transform = CGAffineTransform.identity + } + }, + completion: { [weak self] _ in + self?.badgeImageView.isHidden = false + self?.animateBadge() + } + ) + } + + private func animateBadge() { + UIView.animate( + withDuration: 0.2, + animations: { [badgeImageView, badgeWidth] in + badgeImageView.transform = CGAffineTransform(scaleX: 12.0 / badgeWidth, y: 12.0 / badgeWidth) + }, completion: { [badgeImageView, badgeWidth] _ in + UIView.animate(withDuration: 0.15) { + badgeImageView.transform = CGAffineTransform(scaleX: 8.0 / badgeWidth, y: 8.0 / badgeWidth) + } + } + ) + } + } +} diff --git a/TwoFAS/TwoFAS/Root/Modules/Tokens/View/TokensViewController.swift b/TwoFAS/TwoFAS/Root/Modules/Tokens/View/TokensViewController.swift index 7a56a64a..02f4bc1f 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Tokens/View/TokensViewController.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Tokens/View/TokensViewController.swift @@ -26,6 +26,8 @@ final class TokensViewController: UIViewController { var addButton: UIBarButtonItem? { navigationItem.rightBarButtonItem } + var newsButton: UIBarButtonItem? + private(set) var tokensView: TokensView! private(set) var dataSource: UICollectionViewDiffableDataSource!