From f4a2792b0fc904c4dfc6b195bfd5684bc20467cb Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jul 2021 13:21:05 +0800 Subject: [PATCH 1/8] chore: update cell highlighted color for Dark Mode --- .../xcschemes/xcschememanagement.plist | 4 +- Mastodon/Coordinator/SceneCoordinator.swift | 5 ++- Mastodon/Diffiable/Section/PollSection.swift | 6 +-- Mastodon/Extension/UIView.swift | 15 +++++++- Mastodon/Generated/Assets.swift | 4 ++ .../Colors/Background/Cell/Contents.json | 9 +++++ .../Cell/highlight.colorset/Contents.json | 38 +++++++++++++++++++ .../Cell/separator.colorset/Contents.json | 38 +++++++++++++++++++ .../Contents.json | 6 +-- .../system.background.colorset/Contents.json | 6 +-- .../Contents.json | 6 +-- .../Contents.json | 6 +-- .../Contents.json | 6 +-- .../Cell/AutoCompleteTableViewCell.swift | 5 +++ .../NotificationStatusTableViewCell.swift | 6 ++- .../NotificationTableViewCell.swift | 5 +++ .../Scene/Search/SearchViewController.swift | 1 + .../TableviewCell/StatusTableViewCell.swift | 7 +++- 18 files changed, 148 insertions(+), 25 deletions(-) create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Background/Cell/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Background/Cell/highlight.colorset/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Background/Cell/separator.colorset/Contents.json diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index f1135b12..da1d80e9 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 20 + 21 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -37,7 +37,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 21 + 20 SuppressBuildableAutocreation diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index a12a72d9..fbe0ced5 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -279,7 +279,10 @@ private extension SceneCoordinator { scheme == "http" || scheme == "https" else { return nil } - viewController = SFSafariViewController(url: url) + let _viewController = SFSafariViewController(url: url) + _viewController.preferredControlTintColor = Asset.Colors.brandBlue.color + viewController = _viewController + case .alertController(let alertController): if let popoverPresentationController = alertController.popoverPresentationController { assert( diff --git a/Mastodon/Diffiable/Section/PollSection.swift b/Mastodon/Diffiable/Section/PollSection.swift index 3c31d9b9..581620c0 100644 --- a/Mastodon/Diffiable/Section/PollSection.swift +++ b/Mastodon/Diffiable/Section/PollSection.swift @@ -67,13 +67,13 @@ extension PollSection { cell.pollOptionView.checkmarkBackgroundView.isHidden = true cell.pollOptionView.checkmarkImageView.isHidden = true case .off: - cell.pollOptionView.checkmarkBackgroundView.backgroundColor = .systemBackground - cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.systemGray3.cgColor + cell.pollOptionView.checkmarkBackgroundView.backgroundColor = Asset.Colors.Background.tertiarySystemBackground.color + cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = Asset.Colors.Background.Cell.highlight.color.withAlphaComponent(0.3).cgColor cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 1 cell.pollOptionView.checkmarkBackgroundView.isHidden = false cell.pollOptionView.checkmarkImageView.isHidden = true case .on: - cell.pollOptionView.checkmarkBackgroundView.backgroundColor = .systemBackground + cell.pollOptionView.checkmarkBackgroundView.backgroundColor = Asset.Colors.Background.tertiarySystemBackground.color cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.clear.cgColor cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 0 cell.pollOptionView.checkmarkBackgroundView.isHidden = false diff --git a/Mastodon/Extension/UIView.swift b/Mastodon/Extension/UIView.swift index 05940f7b..50dd08f1 100644 --- a/Mastodon/Extension/UIView.swift +++ b/Mastodon/Extension/UIView.swift @@ -7,12 +7,23 @@ import UIKit -// MARK: - Convinience view creation method +// MARK: - Convenience view creation method extension UIView { + + static let separatorColor: UIColor = { + UIColor(dynamicProvider: { collection in + switch collection.userInterfaceStyle { + case .dark: + return Asset.Colors.Background.Cell.separator.color + default: + return .separator + } + }) + }() static var separatorLine: UIView { let line = UIView() - line.backgroundColor = .separator + line.backgroundColor = UIView.separatorColor return line } diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 4740c938..da2eee46 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -32,6 +32,10 @@ internal enum Asset { } internal enum Colors { internal enum Background { + internal enum Cell { + internal static let highlight = ColorAsset(name: "Colors/Background/Cell/highlight") + internal static let separator = ColorAsset(name: "Colors/Background/Cell/separator") + } internal enum Poll { internal static let disabled = ColorAsset(name: "Colors/Background/Poll/disabled") } diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/Cell/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/Cell/Contents.json new file mode 100644 index 00000000..6e965652 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/Cell/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/Cell/highlight.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/Cell/highlight.colorset/Contents.json new file mode 100644 index 00000000..da756e55 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/Cell/highlight.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "214", + "green" : "209", + "red" : "209" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x6E", + "green" : "0x57", + "red" : "0x4F" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/Cell/separator.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/Cell/separator.colorset/Contents.json new file mode 100644 index 00000000..8ae75b24 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/Cell/separator.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.839", + "green" : "0.820", + "red" : "0.820" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.431", + "green" : "0.341", + "red" : "0.310" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.grouped.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.grouped.system.background.colorset/Contents.json index acd80352..69752587 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.grouped.system.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.grouped.system.background.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "67", - "green" : "53", - "red" : "49" + "blue" : "0x43", + "green" : "0x35", + "red" : "0x31" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/system.background.colorset/Contents.json index 01c3e3ff..f09411b7 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Background/system.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/system.background.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "55", - "green" : "44", - "red" : "40" + "blue" : "0x37", + "green" : "0x2C", + "red" : "0x28" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/system.elevated.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/system.elevated.background.colorset/Contents.json index dd6cbfd9..147cca83 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Background/system.elevated.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/system.elevated.background.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "55", - "green" : "44", - "red" : "40" + "blue" : "0x37", + "green" : "0x2C", + "red" : "0x28" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.background.colorset/Contents.json index 9fa2b261..b769998d 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.background.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x3C", - "green" : "0x3A", - "red" : "0x3A" + "blue" : "0.216", + "green" : "0.173", + "red" : "0.157" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.grouped.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.grouped.background.colorset/Contents.json index 5da572b1..06932c34 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.grouped.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/tertiary.system.grouped.background.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x3C", - "green" : "0x3A", - "red" : "0x3A" + "blue" : "0.263", + "green" : "0.208", + "red" : "0.192" } }, "idiom" : "universal" diff --git a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift b/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift index 6b996ba1..f150dd75 100644 --- a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift +++ b/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift @@ -74,6 +74,11 @@ extension AutoCompleteTableViewCell { private func _init() { backgroundColor = .clear + selectedBackgroundView = { + let view = UIView() + view.backgroundColor = Asset.Colors.Background.Cell.highlight.color + return view + }() let topPaddingView = UIView() let bottomPaddingView = UIView() diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index f4138f09..d2231d4f 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -112,7 +112,11 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { extension NotificationStatusTableViewCell { func configure() { backgroundColor = Asset.Colors.Background.systemBackground.color - + selectedBackgroundView = { + let view = UIView() + view.backgroundColor = Asset.Colors.Background.Cell.highlight.color + return view + }() let containerStackView = UIStackView() containerStackView.axis = .horizontal containerStackView.alignment = .top diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index 375e4c2b..15fa21c6 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -130,6 +130,11 @@ final class NotificationTableViewCell: UITableViewCell { extension NotificationTableViewCell { func configure() { backgroundColor = Asset.Colors.Background.systemBackground.color + selectedBackgroundView = { + let view = UIView() + view.backgroundColor = Asset.Colors.Background.Cell.highlight.color + return view + }() let containerStackView = UIStackView() containerStackView.axis = .vertical diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 9a256fb5..51b88e12 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -103,6 +103,7 @@ final class SearchViewController: UIViewController, NeedsDependency { tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .singleLine tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + tableView.separatorColor = UIView.separatorColor return tableView }() diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index ceb211a7..26230453 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -103,7 +103,12 @@ extension StatusTableViewCell { private func _init() { backgroundColor = Asset.Colors.Background.systemBackground.color - + selectedBackgroundView = { + let view = UIView() + view.backgroundColor = Asset.Colors.Background.Cell.highlight.color + return view + }() + statusView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(statusView) NSLayoutConstraint.activate([ From 083b416648e90b19609d3cd0bcd1bd1bb429c4dd Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jul 2021 14:01:17 +0800 Subject: [PATCH 2/8] fix: cell reuse may not reset image thumbnail download task issue --- .../Scene/Share/View/TableviewCell/StatusTableViewCell.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 26230453..8656a2b6 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -74,6 +74,7 @@ final class StatusTableViewCell: UITableViewCell, StatusCell { override func prepareForReuse() { super.prepareForReuse() selectionStyle = .default + statusView.statusMosaicImageViewContainer.reset() statusView.contentMetaText.textView.isSelectable = false statusView.updateContentWarningDisplay(isHidden: true, animated: false) statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = true From 6c6ab615c78a49aeb1f7a4b3df648973538eb369 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jul 2021 14:01:31 +0800 Subject: [PATCH 3/8] fix: profile avatar can not preview issue --- Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index abeac985..df1eaeb8 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -86,7 +86,11 @@ final class ProfileHeaderView: UIView { } static let avatarImageViewOverlayBlurEffect = UIBlurEffect(style: .systemUltraThinMaterialDark) - let avatarImageViewOverlayVisualEffectView = UIVisualEffectView(effect: nil) + let avatarImageViewOverlayVisualEffectView: UIVisualEffectView = { + let visualEffectView = UIVisualEffectView(effect: nil) + visualEffectView.isUserInteractionEnabled = false + return visualEffectView + }() let editAvatarBackgroundView: UIView = { let view = UIView() From 11180ae62d486d4433afe40e9e8fcc4bfc06bc2f Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jul 2021 14:01:53 +0800 Subject: [PATCH 4/8] fix: notification not trigger update issue --- Mastodon/Diffiable/Section/NotificationSection.swift | 4 +++- .../Scene/Notification/NotificationViewController.swift | 7 +++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index fc88fdde..267f47b7 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -30,7 +30,9 @@ extension NotificationSection { guard let dependency = dependency else { return nil } switch notificationItem { case .notification(let objectID, let attribute): - let notification = managedObjectContext.object(with: objectID) as! MastodonNotification + guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification else { + return UITableViewCell() + } guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.typeRaw) else { // filter out invalid type using predicate assertionFailure() diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 1a1672bf..8ac57a9f 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -143,10 +143,9 @@ extension NotificationViewController { tableView.deselectRow(with: transitionCoordinator, animated: animated) - // fetch latest if has unread push notification - if context.notificationService.hasUnreadPushNotification.value { - viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self) - } + // fetch latest notification when will appear + viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self) + // needs trigger manually after onboarding dismiss setNeedsStatusBarAppearanceUpdate() From ff4e20950dba686ae54f3f2d510a2ca9673c737a Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jul 2021 14:07:24 +0800 Subject: [PATCH 5/8] fix: disable cell frame cache for notification list due to data source not stable --- .../NotificationViewController.swift | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 8ac57a9f..200ea2f2 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -192,33 +192,34 @@ extension NotificationViewController { extension NotificationViewController: StatusTableViewControllerAspect { } // MARK: - TableViewCellHeightCacheableContainer -extension NotificationViewController: TableViewCellHeightCacheableContainer { - var cellFrameCache: NSCache { - viewModel.cellFrameCache - } - - func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } - let key = item.hashValue - let frame = cell.frame - viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key)) - } +//extension NotificationViewController: TableViewCellHeightCacheableContainer { +// var cellFrameCache: NSCache { +// viewModel.cellFrameCache +// } +// +// func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { +// guard let diffableDataSource = viewModel.diffableDataSource else { return } +// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } +// let key = item.hashValue +// let frame = cell.frame +// viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key)) +// } +// +// func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { +// guard let diffableDataSource = viewModel.diffableDataSource else { return UITableView.automaticDimension } +// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension } +// guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else { +// if case .bottomLoader = item { +// return TimelineLoaderTableViewCell.cellHeight +// } else { +// return UITableView.automaticDimension +// } +// } +// +// return ceil(frame.height) +// } +//} - func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - guard let diffableDataSource = viewModel.diffableDataSource else { return UITableView.automaticDimension } - guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension } - guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else { - if case .bottomLoader = item { - return TimelineLoaderTableViewCell.cellHeight - } else { - return UITableView.automaticDimension - } - } - - return ceil(frame.height) - } -} // MARK: - UITableViewDelegate extension NotificationViewController: UITableViewDelegate { From aca776ed17ec1c956fc39e0944cd3325991f3ed9 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jul 2021 14:09:11 +0800 Subject: [PATCH 6/8] fix: profile display name layout issue --- Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index df1eaeb8..ad9c3dbc 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -347,10 +347,9 @@ extension ProfileHeaderView { nameMetaText.textView.translatesAutoresizingMaskIntoConstraints = false displayNameStackView.addSubview(nameMetaText.textView) NSLayoutConstraint.activate([ - nameMetaText.textView.topAnchor.constraint(equalTo: nameTextField.topAnchor), + nameMetaText.textView.centerYAnchor.constraint(equalTo: nameTextField.centerYAnchor), nameMetaText.textView.leadingAnchor.constraint(equalTo: nameTextField.leadingAnchor), nameMetaText.textView.trailingAnchor.constraint(equalTo: nameTextField.trailingAnchor), - nameMetaText.textView.bottomAnchor.constraint(equalTo: nameTextField.bottomAnchor), ]) nameContainerStackView.addArrangedSubview(displayNameStackView) From a614e0c15678c391a3d03d71385c07fe01988105 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jul 2021 19:48:26 +0800 Subject: [PATCH 7/8] fix: line break not works for

tag issue --- Mastodon.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 86bf61f1..3e8f8fdc 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -4782,7 +4782,7 @@ repositoryURL = "https://github.com/TwidereProject/MetaTextView.git"; requirement = { kind = exactVersion; - version = 1.2.3; + version = 1.2.4; }; }; DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = { From 86da7a8ba1ae1913778af70252deb24f4b321510 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jul 2021 19:48:56 +0800 Subject: [PATCH 8/8] fix: GIF not load in some scene issue --- Mastodon.xcodeproj/project.pbxproj | 17 ------ .../xcschemes/xcschememanagement.plist | 4 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Section/NotificationSection.swift | 23 +++++--- .../Diffiable/Section/StatusSection.swift | 54 ++++++------------- Mastodon/Extension/ActiveLabel.swift | 9 ++++ Mastodon/Helper/MastodonStatusContent.swift | 2 +- ...der+UITableViewDataSourcePrefetching.swift | 51 ++++++++---------- .../AsyncHomeTimelineViewController.swift | 12 ----- .../HomeTimeline/HomeTimelineViewModel.swift | 7 ++- .../Paging/Image/MediaPreviewImageView.swift | 9 ++-- .../MediaPreviewImageViewController.swift | 40 ++++++++++---- .../Image/MediaPreviewImageViewModel.swift | 26 +++++---- .../NotificationStatusTableViewCell.swift | 11 ++-- .../NotificationTableViewCell.swift | 10 ++-- ...ontextMenuImagePreviewViewController.swift | 25 ++++++--- .../Scene/Share/View/Content/StatusView.swift | 15 +++--- .../Control/AvatarStackContainerButton.swift | 8 ++- .../ViewModel/MosaicImageViewModel.swift | 4 +- ...astodonAttachmentService+UploadState.swift | 1 - .../MastodonAttachmentService.swift | 1 - Mastodon/State/AppContext.swift | 25 --------- 22 files changed, 175 insertions(+), 183 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 3e8f8fdc..04e7da74 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -273,7 +273,6 @@ DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; - DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; }; DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; }; DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; }; @@ -1146,7 +1145,6 @@ DBB525082611EAC0002F1F29 /* Tabman in Frameworks */, 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, - DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */, DB0E2D2E26833FF700865C3C /* NukeFLAnimatedImagePlugin in Frameworks */, @@ -2677,7 +2675,6 @@ 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, 2D42FF6025C8177C004A627A /* ActiveLabel */, DB0140BC25C40D7500F9F3CF /* CommonOSLog */, - DB5086B725CC0D6400C2C187 /* Kingfisher */, 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, 2D939AC725EE14620076FA61 /* CropViewController */, DB9A487D2603456B008B817C /* UITextView+Placeholder */, @@ -2871,7 +2868,6 @@ 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */, DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, - DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */, 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */, @@ -4801,14 +4797,6 @@ minimumVersion = 4.1.0; }; }; - DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/onevcat/Kingfisher.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 6.1.0; - }; - }; DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; @@ -4938,11 +4926,6 @@ package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; productName = AlamofireImage; }; - DB5086B725CC0D6400C2C187 /* Kingfisher */ = { - isa = XCSwiftPackageProductDependency; - package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */; - productName = Kingfisher; - }; DB68050F2637D0F800430867 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index da1d80e9..f1135b12 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 21 + 20 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -37,7 +37,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 20 + 21 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8414b940..817624a3 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -114,8 +114,8 @@ "repositoryURL": "https://github.com/TwidereProject/MetaTextView.git", "state": { "branch": null, - "revision": "5b86b386464be8a6da5383aa714c458c07da6c01", - "version": "1.2.3" + "revision": "28e53130d16f12e0eeb479d83b77a0a718ef2088", + "version": "1.2.4" } }, { diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 267f47b7..820f9d4b 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -11,6 +11,7 @@ import CoreDataStack import Foundation import MastodonSDK import UIKit +import Nuke enum NotificationSection: Equatable, Hashable { case main @@ -72,10 +73,13 @@ extension NotificationSection { } .store(in: &cell.disposeBag) if let url = notification.account.avatarImageURL() { - cell.avatarImageView.af.setImage( - withURL: url, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) + cell.avatarImageViewTask = Nuke.loadImage( + with: url, + options: ImageLoadingOptions( + placeholder: UIImage.placeholder(color: .systemFill), + transition: .fadeIn(duration: 0.2) + ), + into: cell.avatarImageView ) } cell.avatarImageView.gesture().sink { [weak cell] _ in @@ -113,10 +117,13 @@ extension NotificationSection { cell.actionLabel.text = actionText + " ยท " + timeText cell.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict) if let url = notification.account.avatarImageURL() { - cell.avatarImageView.af.setImage( - withURL: url, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) + cell.avatarImageViewTask = Nuke.loadImage( + with: url, + options: ImageLoadingOptions( + placeholder: UIImage.placeholder(color: .systemFill), + transition: .fadeIn(duration: 0.2) + ), + into: cell.avatarImageView ) } cell.avatarImageView.gesture().sink { [weak cell] _ in diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index dd244eba..c5995124 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -547,7 +547,13 @@ extension StatusSection { // name let author = (status.reblog ?? status).author let nameContent = author.displayNameWithFallback - cell.statusView.nameLabel.configure(content: nameContent, emojiDict: author.emojiDict) + MastodonStatusContent.parseResult(content: nameContent, emojiDict: author.emojiDict) + .receive(on: DispatchQueue.main) + .sink { [weak cell] parseResult in + guard let cell = cell else { return } + cell.statusView.nameLabel.configure(contentParseResult: parseResult) + } + .store(in: &cell.disposeBag) // username cell.statusView.usernameLabel.text = "@" + author.acct // avatar @@ -571,9 +577,10 @@ extension StatusSection { ) { // set content do { + let status = status.reblog ?? status let content = MastodonContent( - content: (status.reblog ?? status).content, - emojis: (status.reblog ?? status).emojiMeta + content: status.content, + emojis: status.emojiMeta ) let metaContent = try MastodonMetaContent.convert(document: content) cell.statusView.contentMetaText.configure(content: metaContent) @@ -648,46 +655,19 @@ extension StatusSection { let isSingleMosaicLayout = mosaics.count == 1 - // set link preview -// cell.statusView.linkPreview.isHidden = true -// -// var _firstURL: URL? = { -// for entity in cell.statusView.activeTextLabel.activeEntities { -// guard case let .url(_, _, url, _) = entity.type else { continue } -// return URL(string: url) -// } -// return nil -// }() -// -// if let url = _firstURL { -// Future { promise in -// LPMetadataProvider().startFetchingMetadata(for: url) { meta, error in -// if let error = error { -// promise(.failure(error)) -// } else { -// promise(.success(meta)) -// } -// } -// } -// .receive(on: RunLoop.main) -// .sink { _ in -// // do nothing -// } receiveValue: { [weak cell] meta in -// guard let meta = meta else { return } -// guard let cell = cell else { return } -// cell.statusView.linkPreview.metadata = meta -// cell.statusView.linkPreview.isHidden = false -// } -// .store(in: &cell.disposeBag) -// } - // set image let imageSize = CGSize( width: mosaic.imageViewSize.width * imageView.traitCollection.displayScale, height: mosaic.imageViewSize.height * imageView.traitCollection.displayScale ) + let url: URL? = { + if UIDevice.current.userInterfaceIdiom == .phone { + return meta.previewURL ?? meta.url + } + return meta.url + }() let request = ImageRequest( - url: meta.url, + url: url, processors: [ ImageProcessors.Resize( size: imageSize, diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift index b4f89e71..ebb82655 100644 --- a/Mastodon/Extension/ActiveLabel.swift +++ b/Mastodon/Extension/ActiveLabel.swift @@ -57,6 +57,15 @@ extension ActiveLabel { } +extension ActiveLabel { + func configure(text: String) { + attributedText = nil + activeEntities.removeAll() + self.text = text + accessibilityLabel = text + } +} + extension ActiveLabel { /// status content diff --git a/Mastodon/Helper/MastodonStatusContent.swift b/Mastodon/Helper/MastodonStatusContent.swift index 1e52f150..d19463a8 100755 --- a/Mastodon/Helper/MastodonStatusContent.swift +++ b/Mastodon/Helper/MastodonStatusContent.swift @@ -15,7 +15,7 @@ enum MastodonStatusContent { typealias EmojiShortcode = String typealias EmojiDict = [EmojiShortcode: URL] - static let workingQueue = DispatchQueue(label: "org.joinmastodon.app.ActiveLabel.working-queue", qos: .userInteractive) + static let workingQueue = DispatchQueue(label: "org.joinmastodon.app.ActiveLabel.working-queue", qos: .userInteractive, attributes: .concurrent) static func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> AnyPublisher { return Future { promise in diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift index a9e1898a..5d6e81d5 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift @@ -14,27 +14,27 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { // prefetch reply status guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } let domain = activeMastodonAuthenticationBox.domain - - var statusObjectIDs: [NSManagedObjectID] = [] - for item in items(indexPaths: indexPaths) { - switch item { - case .homeTimelineIndex(let objectID, _): - let homeTimelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex - statusObjectIDs.append(homeTimelineIndex.status.objectID) - case .status(let objectID, _): - statusObjectIDs.append(objectID) - default: - continue - } - } - - let backgroundManagedObjectContext = context.backgroundManagedObjectContext - backgroundManagedObjectContext.perform { [weak self] in + let items = self.items(indexPaths: indexPaths) + + let managedObjectContext = context.managedObjectContext + managedObjectContext.perform { [weak self] in guard let self = self else { return } - for objectID in statusObjectIDs { - let status = backgroundManagedObjectContext.object(with: objectID) as! Status - - // fetch in-reply info if needs + + var statuses: [Status] = [] + for item in items { + switch item { + case .homeTimelineIndex(let objectID, _): + guard let homeTimelineIndex = try? managedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex else { continue } + statuses.append(homeTimelineIndex.status) + case .status(let objectID, _): + guard let status = try? managedObjectContext.existingObject(with: objectID) as? Status else { continue } + statuses.append(status) + default: + continue + } + } + + for status in statuses { if let replyToID = status.inReplyToID, status.replyTo == nil { self.context.statusPrefetchingService.prefetchReplyTo( domain: domain, @@ -44,12 +44,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { authorizationBox: activeMastodonAuthenticationBox ) } - -// self.context.statusContentCacheService.prefetch( -// content: (status.reblog ?? status).content, -// emojiDict: (status.reblog ?? status).emojiDict -// ) - } - } - } + } // end for in + } // end context.perform + } // end func } diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift index 24736480..c8ccfdd3 100644 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift @@ -18,10 +18,6 @@ import MastodonSDK import AlamofireImage import AsyncDisplayKit -#if DEBUG -import GDPerformanceView_Swift -#endif - final class AsyncHomeTimelineViewController: ASDKViewController, NeedsDependency, MediaPreviewableViewController { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } @@ -107,7 +103,6 @@ extension AsyncHomeTimelineViewController { #if DEBUG // long press to trigger debug menu settingBarButtonItem.menu = debugMenu - PerformanceMonitor.shared().delegate = self #else settingBarButtonItem.target = self settingBarButtonItem.action = #selector(AsyncHomeTimelineViewController.settingBarButtonItemPressed(_:)) @@ -548,13 +543,6 @@ extension AsyncHomeTimelineViewController: StatusTableViewControllerNavigateable } } -#if DEBUG -extension AsyncHomeTimelineViewController: PerformanceMonitorDelegate { - func performanceMonitor(didReport performanceReport: PerformanceReport) { - // print(performanceReport) - } -} -#endif // MARK: - ASTableDelegate extension AsyncHomeTimelineViewController: ASTableDelegate { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index fdbbfba9..d22c52be 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -77,7 +77,12 @@ final class HomeTimelineViewModel: NSObject { let fetchRequest = HomeTimelineIndex.sortedFetchRequest fetchRequest.fetchBatchSize = 20 fetchRequest.returnsObjectsAsFaults = false - fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(HomeTimelineIndex.status)] + fetchRequest.relationshipKeyPathsForPrefetching = [ + #keyPath(HomeTimelineIndex.status), + #keyPath(HomeTimelineIndex.status.author), + #keyPath(HomeTimelineIndex.status.reblog), + #keyPath(HomeTimelineIndex.status.reblog.author), + ] let controller = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: context.managedObjectContext, diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift index aa11e494..05f2ce70 100644 --- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift +++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift @@ -8,11 +8,12 @@ import os.log import func AVFoundation.AVMakeRect import UIKit +import FLAnimatedImage final class MediaPreviewImageView: UIScrollView { - let imageView: UIImageView = { - let imageView = UIImageView() + let imageView: FLAnimatedImageView = { + let imageView = FLAnimatedImageView() imageView.contentMode = .scaleAspectFit imageView.clipsToBounds = true imageView.isUserInteractionEnabled = true @@ -120,7 +121,9 @@ extension MediaPreviewImageView { } }() imageView.frame = CGRect(origin: .zero, size: imageViewSize) - imageView.image = image + if imageView.image == nil { + imageView.image = image + } contentSize = imageViewSize contentInset = imageContentInset diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift index e1f2736f..5ec82db9 100644 --- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift +++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import Nuke protocol MediaPreviewImageViewControllerDelegate: AnyObject { func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, tapGestureRecognizerDidTrigger tapGestureRecognizer: UITapGestureRecognizer) @@ -68,17 +69,36 @@ extension MediaPreviewImageViewController { let previewImageViewContextMenuInteraction = UIContextMenuInteraction(delegate: self) previewImageView.addInteraction(previewImageViewContextMenuInteraction) - - viewModel.image - .receive(on: RunLoop.main) // use RunLoop prevent set image during zooming (TODO: handle transitioning state) - .sink { [weak self] image in - guard let self = self else { return } - guard let image = image else { return } - self.previewImageView.imageView.image = image - self.previewImageView.setup(image: image, container: self.previewImageView, forceUpdate: true) - self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText + + switch viewModel.item { + case .local(let meta): + self.previewImageView.imageView.image = meta.image + self.previewImageView.setup(image: meta.image, container: self.previewImageView, forceUpdate: true) + self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText + case .status(let meta): + Nuke.loadImage( + with: meta.url, + into: self.previewImageView.imageView + ) { result in + switch result { + case .failure(let error): + break + case .success(let response): + self.previewImageView.setup(image: response.image, container: self.previewImageView, forceUpdate: true) + self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText + } } - .store(in: &disposeBag) + } +// viewModel.image +// .receive(on: RunLoop.main) // use RunLoop prevent set image during zooming (TODO: handle transitioning state) +// .sink { [weak self] image in +// guard let self = self else { return } +// guard let image = image else { return } +// self.previewImageView.imageView.image = image +// self.previewImageView.setup(image: image, container: self.previewImageView, forceUpdate: true) +// self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText +// } +// .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift index c9afac8c..a6163a8c 100644 --- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift +++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift @@ -8,9 +8,11 @@ import os.log import UIKit import Combine -import AlamofireImage +import Nuke class MediaPreviewImageViewModel { + + var disposeBag = Set() // input let item: ImagePreviewItem @@ -25,16 +27,20 @@ class MediaPreviewImageViewModel { self.altText = meta.altText let url = meta.url - ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in - guard let self = self else { return } - switch response.result { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription) - case .success(let image): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription) - self.image.value = image + + ImagePipeline.shared.imagePublisher(with: url) + .sink { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription) + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + self.image.value = response.image } - }) + .store(in: &disposeBag) } init(meta: LocalImagePreviewMeta) { diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index d2231d4f..83fa5009 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -11,6 +11,8 @@ import UIKit import ActiveLabel import MetaTextView import Meta +import FLAnimatedImage +import Nuke final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { static let actionImageBorderWidth: CGFloat = 2 @@ -18,9 +20,10 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { var disposeBag = Set() var pollCountdownSubscription: AnyCancellable? var delegate: NotificationTableViewCellDelegate? - + + var avatarImageViewTask: ImageTask? let avatarImageView: UIImageView = { - let imageView = UIImageView() + let imageView = FLAnimatedImageView() imageView.layer.cornerRadius = 4 imageView.layer.cornerCurve = .continuous imageView.clipsToBounds = true @@ -88,12 +91,12 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { override func prepareForReuse() { super.prepareForReuse() - avatarImageView.af.cancelImageRequest() + avatarImageViewTask?.cancel() + avatarImageViewTask = nil statusView.updateContentWarningDisplay(isHidden: true, animated: false) statusView.pollTableView.dataSource = nil statusView.playerContainerView.reset() statusView.playerContainerView.isHidden = true - disposeBag.removeAll() } diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index 15fa21c6..30b4ce1b 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -12,6 +12,8 @@ import UIKit import Meta import MetaTextView import ActiveLabel +import FLAnimatedImage +import Nuke protocol NotificationTableViewCellDelegate: AnyObject { var context: AppContext! { get } @@ -37,9 +39,10 @@ final class NotificationTableViewCell: UITableViewCell { var disposeBag = Set() var delegate: NotificationTableViewCellDelegate? - + + var avatarImageViewTask: ImageTask? let avatarImageView: UIImageView = { - let imageView = UIImageView() + let imageView = FLAnimatedImageView() imageView.layer.cornerRadius = 4 imageView.layer.cornerCurve = .continuous imageView.clipsToBounds = true @@ -112,7 +115,8 @@ final class NotificationTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() - avatarImageView.af.cancelImageRequest() + avatarImageViewTask?.cancel() + avatarImageViewTask = nil disposeBag.removeAll() } diff --git a/Mastodon/Scene/Share/ContextMenu/ImagePreview/ContextMenuImagePreviewViewController.swift b/Mastodon/Scene/Share/ContextMenu/ImagePreview/ContextMenuImagePreviewViewController.swift index 2a5ba492..28b13e2f 100644 --- a/Mastodon/Scene/Share/ContextMenu/ImagePreview/ContextMenuImagePreviewViewController.swift +++ b/Mastodon/Scene/Share/ContextMenu/ImagePreview/ContextMenuImagePreviewViewController.swift @@ -8,19 +8,27 @@ import func AVFoundation.AVMakeRect import UIKit import Combine +import Nuke +import FLAnimatedImage final class ContextMenuImagePreviewViewController: UIViewController { var disposeBag = Set() var viewModel: ContextMenuImagePreviewViewModel! - + + var imageTask: ImageTask? let imageView: UIImageView = { - let imageView = UIImageView() + let imageView = FLAnimatedImageView() imageView.contentMode = .scaleAspectFill imageView.layer.masksToBounds = true return imageView }() + + deinit { + imageTask?.cancel() + imageTask = nil + } } @@ -47,12 +55,13 @@ extension ContextMenuImagePreviewViewController { .sink { [weak self] url in guard let self = self else { return } guard let url = url else { return } - self.imageView.af.setImage( - withURL: url, - placeholderImage: self.viewModel.thumbnail, - imageTransition: .crossDissolve(0.2), - runImageTransitionIfCached: true, - completion: nil + self.imageTask = Nuke.loadImage( + with: url, + options: ImageLoadingOptions( + placeholder: self.viewModel.thumbnail, + transition: .fadeIn(duration: 0.2) + ), + into: self.imageView ) } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 2b51f028..7ed828fa 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -95,7 +95,12 @@ final class StatusView: UIView { view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile return view }() - let avatarImageView: UIImageView = FLAnimatedImageView() + let avatarImageView: UIImageView = { + let imageView = FLAnimatedImageView() + imageView.layer.shouldRasterize = true + imageView.layer.rasterizationScale = UIScreen.main.scale + return imageView + }() let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton() let nameLabel: ActiveLabel = { @@ -217,6 +222,7 @@ final class StatusView: UIView { metaText.textView.textContainer.lineFragmentPadding = 0 metaText.textView.textContainerInset = .zero metaText.textView.layer.masksToBounds = false + let paragraphStyle: NSMutableParagraphStyle = { let style = NSMutableParagraphStyle() style.lineSpacing = 5 @@ -234,7 +240,7 @@ final class StatusView: UIView { ] return metaText }() - + private let headerInfoLabelTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer var isRevealing = true @@ -400,11 +406,6 @@ extension StatusView { statusContainerStackView.addArrangedSubview(contentMetaText.textView) contentMetaText.textView.setContentCompressionResistancePriority(.required - 1, for: .vertical) - // TODO: - // link preview - // statusContainerStackView.addArrangedSubview(linkPreview) - // linkPreview.setContentHuggingPriority(.defaultHigh, for: .vertical) - // image statusContainerStackView.addArrangedSubview(statusMosaicImageViewContainer) diff --git a/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift b/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift index 965d710d..d16a952f 100644 --- a/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift +++ b/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift @@ -25,7 +25,7 @@ final class AvatarStackContainerButton: UIControl { static let maskOffset: CGFloat = 2 // UIControl.Event - Application: 0x0F000000 - static let primaryAction = UIControl.Event(rawValue: 1 << 25) // 0x01000000 + static let primaryAction = UIControl.Event(rawValue: 1 << 25) // 0x01000000 var primaryActionState: UIControl.State = .normal let topLeadingAvatarStackedImageView = AvatarStackedImageView() @@ -46,6 +46,12 @@ final class AvatarStackContainerButton: UIControl { extension AvatarStackContainerButton { private func _init() { + topLeadingAvatarStackedImageView.layer.shouldRasterize = true + topLeadingAvatarStackedImageView.layer.rasterizationScale = UIScreen.main.scale + + bottomTrailingAvatarStackedImageView.layer.shouldRasterize = true + bottomTrailingAvatarStackedImageView.layer.rasterizationScale = UIScreen.main.scale + topLeadingAvatarStackedImageView.translatesAutoresizingMaskIntoConstraints = false addSubview(topLeadingAvatarStackedImageView) NSLayoutConstraint.activate([ diff --git a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift index 265ce245..5ceb8781 100644 --- a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift @@ -23,7 +23,7 @@ struct MosaicImageViewModel { continue } let mosaicMeta = MosaicMeta( - priviewURL: element.previewURL.flatMap { URL(string: $0) }, + previewURL: element.previewURL.flatMap { URL(string: $0) }, url: url, size: CGSize(width: width, height: height), blurhash: element.blurhash, @@ -39,7 +39,7 @@ struct MosaicImageViewModel { struct MosaicMeta { static let edgeMaxLength: CGFloat = 20 - let priviewURL: URL? + let previewURL: URL? let url: URL let size: CGSize let blurhash: String? diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift index b154d17c..f773115d 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift @@ -8,7 +8,6 @@ import os.log import Foundation import GameplayKit -import Kingfisher import MastodonSDK extension MastodonAttachmentService { diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift index ede5c64b..cafa3145 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift @@ -9,7 +9,6 @@ import os.log import UIKit import Combine import PhotosUI -import Kingfisher import GameplayKit import MobileCoreServices import MastodonSDK diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index ef758951..f1de6dd7 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -11,7 +11,6 @@ import Combine import CoreData import CoreDataStack import AlamofireImage -import Kingfisher class AppContext: ObservableObject { @@ -124,7 +123,6 @@ extension AppContext { func purgeCache() -> AnyPublisher { Publishers.MergeMany([ AppContext.purgeAlamofireImageCache(), - AppContext.purgeKingfisherCache(), AppContext.purgeTemporaryDirectory(), ]) .reduce(0, +) @@ -146,29 +144,6 @@ extension AppContext { .eraseToAnyPublisher() } - private static func purgeKingfisherCache() -> AnyPublisher { - Future { promise in - KingfisherManager.shared.cache.calculateDiskStorageSize { result in - switch result { - case .success(let diskBytes): - KingfisherManager.shared.cache.clearCache() - KingfisherManager.shared.cache.calculateDiskStorageSize { currentResult in - switch currentResult { - case .success(let currentDiskBytes): - let purgedDiskBytes = max(0, Int(diskBytes) - Int(currentDiskBytes)) - promise(.success(purgedDiskBytes)) - case .failure: - promise(.success(0)) - } - } - case .failure: - promise(.success(0)) - } - } - } - .eraseToAnyPublisher() - } - private static func purgeTemporaryDirectory() -> AnyPublisher { Future { promise in AppContext.purgeCacheWorkingQueue.async {