Merge pull request #129 from twofas/feature/TF-1552

[TF-1552] Notification animation and change notifications order
This commit is contained in:
gmachnio 2024-07-30 19:33:07 +02:00 committed by GitHub
commit 2712335a60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 220 additions and 37 deletions

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "NavibarNewsIconBadge.pdf",
"filename" : "Ellipse 10.pdf",
"idiom" : "universal"
}
],

View File

@ -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

View File

@ -8,8 +8,5 @@
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -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")

View File

@ -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 ?? "",

View File

@ -531,7 +531,7 @@ private extension TokensPresenter {
}()
if interactor.hasServices {
updateAddServiceIcon()
updateNaviIcons()
view?.showList()
if Set<CategoryData>(currentServices) != Set<CategoryData>(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
)
}
}
}

View File

@ -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)
}
}
)
}
}
}

View File

@ -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<TokensSection, TokenCell>!