From 16d6c9e823767a6d53ead261ffef9a8db894d374 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 21 May 2024 17:14:32 +0200 Subject: [PATCH 1/4] Set placeholder only if something goes wrong (IOS-238) --- .../Extension/FLAnimatedImageView.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Extension/FLAnimatedImageView.swift b/MastodonSDK/Sources/MastodonUI/Extension/FLAnimatedImageView.swift index 68c7fbc953..41b3ff6dd8 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/FLAnimatedImageView.swift +++ b/MastodonSDK/Sources/MastodonUI/Extension/FLAnimatedImageView.swift @@ -45,11 +45,14 @@ extension FLAnimatedImageView { // cancel task cancelTask() - // set placeholder - image = placeholder - // set image - guard let url = url else { return } + guard let url else { + DispatchQueue.main.async { [weak self] in + self?.image = placeholder + } + return + } + activeAvatarRequestURL = url let avatarRequest = AF.request(url).publishData() avatarRequestCancellable = avatarRequest @@ -78,7 +81,9 @@ extension FLAnimatedImageView { } } case .failure: - break + DispatchQueue.main.async { [weak self] in + self?.image = placeholder + } } } } From 298886d3f576a0b8002d0ce1f52524cbab4eef7f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 21 May 2024 17:43:53 +0200 Subject: [PATCH 2/4] Set placeholder-color for AvatarButton (IOS-238) --- .../Cell/AccountListTableViewCell.swift | 2 +- .../MastodonUI/View/Button/AvatarButton.swift | 18 ++++++------------ .../View/Button/CircleAvatarButton.swift | 6 ++++++ .../View/Content/StatusView+ViewModel.swift | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift index 53bed876ad..322b8e796d 100644 --- a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift +++ b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift @@ -12,7 +12,7 @@ final class AccountListTableViewCell: UITableViewCell { private var _disposeBag = Set() var disposeBag = Set() - let avatarButton = CircleAvatarButton(frame: .zero) + let avatarButton = CircleAvatarButton() let nameLabel = MetaLabel(style: .accountListName) let usernameLabel = MetaLabel(style: .accountListUsername) let badgeButton = BadgeButton() diff --git a/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift b/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift index 6d399c1b2f..ab6f8eabd8 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Button/AvatarButton.swift @@ -17,17 +17,9 @@ open class AvatarButton: UIControl { public var size = CGSize(width: 46, height: 46) public let avatarImageView = AvatarImageView() - public override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - - open func _init() { + public init(avatarPlaceholder: UIImage? = UIImage.placeholder(color: .systemFill)) { + super.init(frame: .zero) + avatarImageView.image = avatarPlaceholder avatarImageView.frame = bounds avatarImageView.translatesAutoresizingMaskIntoConstraints = false addSubview(avatarImageView) @@ -37,7 +29,9 @@ open class AvatarButton: UIControl { accessibilityLabel = L10n.Common.Controls.Status.showUserProfile accessibilityTraits.insert(.image) } - + + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented")} + public override func layoutSubviews() { super.layoutSubviews() diff --git a/MastodonSDK/Sources/MastodonUI/View/Button/CircleAvatarButton.swift b/MastodonSDK/Sources/MastodonUI/View/Button/CircleAvatarButton.swift index 1a6894eb36..6cf2cfadd9 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Button/CircleAvatarButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Button/CircleAvatarButton.swift @@ -14,6 +14,12 @@ public final class CircleAvatarButton: AvatarButton { public var borderColor: UIColor = UIColor.systemFill public var borderWidth: CGFloat = 2.0 + + public init() { + super.init(avatarPlaceholder: .placeholder(color: .systemFill)) + } + + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented")} public override func updateAppearance() { super.updateAppearance() diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index cce4f0f606..6cceb071fc 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -247,7 +247,7 @@ extension StatusView.ViewModel { ) .sink { image, url in let configuration: AvatarImageView.Configuration = { - if let image = image { + if let image { return AvatarImageView.Configuration(image: image) } else { return AvatarImageView.Configuration(url: url) From 9e486a82e68182c307ba02c0e4fab4aa95478a6b Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 21 May 2024 18:01:54 +0200 Subject: [PATCH 3/4] Remove download-placeholder (IOS-238) --- .../Discovery/ForYou/ProfileCardView+ViewModel.swift | 3 +-- .../Scene/Discovery/ForYou/ProfileCardView.swift | 2 +- .../Header/View/ProfileHeaderView+ViewModel.swift | 4 ++-- .../MastodonUI/Extension/FLAnimatedImageView.swift | 12 ++---------- .../FamiliarFollowersDashboardView+ViewModel.swift | 5 ++--- .../MastodonUI/View/ImageView/AvatarImageView.swift | 11 ++--------- 6 files changed, 10 insertions(+), 27 deletions(-) diff --git a/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift b/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift index 05bd54fcaa..f197cfb687 100644 --- a/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift +++ b/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift @@ -75,8 +75,7 @@ extension ProfileCardView.ViewModel { .sink { url in view.avatarButton.avatarImageView.configure( configuration: .init( - url: url, - placeholder: .placeholder(color: .systemGray3) + url: url ) ) view.avatarButton.avatarImageView.configure( diff --git a/Mastodon/Scene/Discovery/ForYou/ProfileCardView.swift b/Mastodon/Scene/Discovery/ForYou/ProfileCardView.swift index e2d92c98ec..bd68c84558 100644 --- a/Mastodon/Scene/Discovery/ForYou/ProfileCardView.swift +++ b/Mastodon/Scene/Discovery/ForYou/ProfileCardView.swift @@ -43,7 +43,7 @@ public final class ProfileCardView: UIView, AXCustomContentProvider { // avatar public let avatarButtonBackgroundView = UIView() - public let avatarButton = AvatarButton() + public let avatarButton = AvatarButton(avatarPlaceholder: .placeholder(color: .systemGray3)) // author name public let authorNameLabel = MetaLabel(style: .profileCardName) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index 7c01850be5..cf5dfcd61c 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -115,9 +115,9 @@ extension ProfileHeaderView.ViewModel { viewDidAppear ) .sink { avatarImageURL, avatarImageEditing, isEditing, _ in + view.avatarButton.avatarImageView.image = avatarImageEditing view.avatarButton.avatarImageView.configure(configuration: .init( - url: (!isEditing || avatarImageEditing == nil) ? avatarImageURL : nil, - placeholder: isEditing ? (avatarImageEditing ?? AvatarImageView.placeholder) : AvatarImageView.placeholder + url: (!isEditing || avatarImageEditing == nil) ? avatarImageURL : nil )) } .store(in: &disposeBag) diff --git a/MastodonSDK/Sources/MastodonUI/Extension/FLAnimatedImageView.swift b/MastodonSDK/Sources/MastodonUI/Extension/FLAnimatedImageView.swift index 41b3ff6dd8..0f2dc7932f 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/FLAnimatedImageView.swift +++ b/MastodonSDK/Sources/MastodonUI/Extension/FLAnimatedImageView.swift @@ -39,19 +39,13 @@ extension FLAnimatedImageView { public func setImage( url: URL?, - placeholder: UIImage? = nil, scaleToSize: CGSize? = nil ) { // cancel task cancelTask() // set image - guard let url else { - DispatchQueue.main.async { [weak self] in - self?.image = placeholder - } - return - } + guard let url else { return } activeAvatarRequestURL = url let avatarRequest = AF.request(url).publishData() @@ -81,9 +75,7 @@ extension FLAnimatedImageView { } } case .failure: - DispatchQueue.main.async { [weak self] in - self?.image = placeholder - } + break } } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift index 2db0a59663..c95ebf7efb 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift @@ -45,7 +45,7 @@ extension FamiliarFollowersDashboardView.ViewModel { let borderWidth = min(1.5, UIFontMetrics.default.scaledValue(for: 1)) for (i, avatarURL) in avatarURLs.enumerated() { - let avatarButton = AvatarButton() + let avatarButton = AvatarButton(avatarPlaceholder: .placeholder(color: .systemGray3)) let origin = CGPoint(x: offset * CGFloat(i), y: 0) let size = CGSize(width: dimension, height: dimension) avatarButton.size = size @@ -53,8 +53,7 @@ extension FamiliarFollowersDashboardView.ViewModel { view.avatarContainerView.addSubview(avatarButton) avatarButton.avatarImageView.configure( configuration: .init( - url: avatarURL, - placeholder: .placeholder(color: .systemGray3) + url: avatarURL ) ) avatarButton.avatarImageView.configure( diff --git a/MastodonSDK/Sources/MastodonUI/View/ImageView/AvatarImageView.swift b/MastodonSDK/Sources/MastodonUI/View/ImageView/AvatarImageView.swift index 0406b17f6c..047582e1ce 100644 --- a/MastodonSDK/Sources/MastodonUI/View/ImageView/AvatarImageView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/ImageView/AvatarImageView.swift @@ -57,21 +57,17 @@ extension AvatarImageView { public struct Configuration { public let url: URL? - public let placeholder: UIImage? - + public init( - url: URL?, - placeholder: UIImage = AvatarImageView.placeholder + url: URL? ) { self.url = url - self.placeholder = placeholder } public init( image: UIImage ) { self.url = nil - self.placeholder = image } } @@ -81,7 +77,6 @@ extension AvatarImageView { self.configuration = configuration guard let url = configuration.url else { - image = configuration.placeholder return } @@ -89,7 +84,6 @@ extension AvatarImageView { case "gif": setImage( url: configuration.url, - placeholder: configuration.placeholder, scaleToSize: imageViewSize ) default: @@ -105,7 +99,6 @@ extension AvatarImageView { af.setImage( withURL: url, - placeholderImage: configuration.placeholder, filter: filter ) } From 47bedff5e51ef9ac4970c77cbdd13016739de5a9 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 21 May 2024 18:11:18 +0200 Subject: [PATCH 4/4] Cleanup (IOS-238) --- .../Scene/Account/AccountListViewModel.swift | 4 +-- .../ForYou/ProfileCardView+ViewModel.swift | 6 +--- .../NotificationView+Configuration.swift | 3 +- .../View/ProfileHeaderView+ViewModel.swift | 4 +-- .../AutoCompleteSection+Diffable.swift | 4 +-- ...liarFollowersDashboardView+ViewModel.swift | 6 +--- .../View/Content/StatusView+ViewModel.swift | 16 ++-------- .../View/Content/UserView+ViewModel.swift | 24 +++++---------- .../View/ImageView/AvatarImageView.swift | 30 ++++--------------- 9 files changed, 23 insertions(+), 74 deletions(-) diff --git a/Mastodon/Scene/Account/AccountListViewModel.swift b/Mastodon/Scene/Account/AccountListViewModel.swift index e081f432c0..afd48a6e7c 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -106,9 +106,7 @@ extension AccountListViewModel { guard let account = authentication.account() else { return } // avatar - cell.avatarButton.avatarImageView.configure( - configuration: .init(url: account.avatarImageURL()) - ) + cell.avatarButton.avatarImageView.configure(with: account.avatarImageURL()) // name do { diff --git a/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift b/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift index f197cfb687..cfcc751135 100644 --- a/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift +++ b/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift @@ -73,11 +73,7 @@ extension ProfileCardView.ViewModel { private func bindUser(view: ProfileCardView) { $authorAvatarImageURL .sink { url in - view.avatarButton.avatarImageView.configure( - configuration: .init( - url: url - ) - ) + view.avatarButton.avatarImageView.configure(with: url) view.avatarButton.avatarImageView.configure( cornerConfiguration: .init(corner: .fixed(radius: 12)) ) diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift index 353f309db5..01ae0ab10f 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift @@ -66,8 +66,7 @@ extension NotificationView { let author = notification.account // author avatar - let configuration = AvatarImageView.Configuration(url: author.avatarImageURL()) - avatarButton.avatarImageView.configure(configuration: configuration) + avatarButton.avatarImageView.configure(with: author.avatarImageURL()) avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12))) // author name diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index cf5dfcd61c..f7d461283f 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -116,9 +116,7 @@ extension ProfileHeaderView.ViewModel { ) .sink { avatarImageURL, avatarImageEditing, isEditing, _ in view.avatarButton.avatarImageView.image = avatarImageEditing - view.avatarButton.avatarImageView.configure(configuration: .init( - url: (!isEditing || avatarImageEditing == nil) ? avatarImageURL : nil - )) + view.avatarButton.avatarImageView.configure(with: (!isEditing || avatarImageEditing == nil) ? avatarImageURL : nil) } .store(in: &disposeBag) // blur for blocking & blockingBy diff --git a/MastodonSDK/Sources/MastodonUI/DataSource/AutoCompleteSection+Diffable.swift b/MastodonSDK/Sources/MastodonUI/DataSource/AutoCompleteSection+Diffable.swift index 3022899f14..ff8dd20f30 100644 --- a/MastodonSDK/Sources/MastodonUI/DataSource/AutoCompleteSection+Diffable.swift +++ b/MastodonSDK/Sources/MastodonUI/DataSource/AutoCompleteSection+Diffable.swift @@ -78,7 +78,7 @@ extension AutoCompleteSection { } cell.subtitleLabel.text = "@" + account.acct cell.avatarImageView.isHidden = false - cell.avatarImageView.configure(configuration: .init(url: URL(string: account.avatar))) + cell.avatarImageView.configure(with: URL(string: account.avatar)) } private static func configureEmoji(cell: AutoCompleteTableViewCell, emoji: Mastodon.Entity.Emoji, isFirst: Bool) { @@ -88,7 +88,7 @@ extension AutoCompleteSection { // cell.subtitleLabel.text = isFirst ? L10n.Scene.Compose.AutoComplete.spaceToAdd : " " cell.subtitleLabel.text = " " cell.avatarImageView.isHidden = false - cell.avatarImageView.configure(configuration: .init(url: URL(string: emoji.url))) + cell.avatarImageView.configure(with: URL(string: emoji.url)) } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift index c95ebf7efb..4896af1982 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift @@ -51,11 +51,7 @@ extension FamiliarFollowersDashboardView.ViewModel { avatarButton.size = size avatarButton.frame = CGRect(origin: origin, size: size) view.avatarContainerView.addSubview(avatarButton) - avatarButton.avatarImageView.configure( - configuration: .init( - url: avatarURL - ) - ) + avatarButton.avatarImageView.configure(with: avatarURL) avatarButton.avatarImageView.configure( cornerConfiguration: .init( corner: .fixed(radius: 7), diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index 6cceb071fc..58bb9df89a 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -241,19 +241,9 @@ extension StatusView.ViewModel { private func bindAuthor(statusView: StatusView) { let authorView = statusView.authorView // avatar - Publishers.CombineLatest( - $authorAvatarImage.removeDuplicates(), - $authorAvatarImageURL.removeDuplicates() - ) - .sink { image, url in - let configuration: AvatarImageView.Configuration = { - if let image { - return AvatarImageView.Configuration(image: image) - } else { - return AvatarImageView.Configuration(url: url) - } - }() - authorView.avatarButton.avatarImageView.configure(configuration: configuration) + $authorAvatarImageURL.removeDuplicates() + .sink { url in + authorView.avatarButton.avatarImageView.configure(with: url) authorView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12))) } .store(in: &disposeBag) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift index c29bdd88d4..17bf4cfade 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift @@ -20,7 +20,6 @@ extension UserView { public var disposeBag = Set() public var observations = Set() - @Published public var authorAvatarImage: UIImage? @Published public var authorAvatarImageURL: URL? @Published public var authorName: MetaContent? @Published public var authorUsername: String? @@ -36,22 +35,13 @@ extension UserView.ViewModel { func bind(userView: UserView) { // avatar - Publishers.CombineLatest( - $authorAvatarImage, - $authorAvatarImageURL - ) - .sink { image, url in - let configuration: AvatarImageView.Configuration = { - if let image = image { - return AvatarImageView.Configuration(image: image) - } else { - return AvatarImageView.Configuration(url: url) - } - }() - userView.avatarButton.avatarImageView.configure(configuration: configuration) - userView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 7))) - } - .store(in: &disposeBag) + $authorAvatarImageURL + .sink { url in + userView.avatarButton.avatarImageView.configure(with: url) + userView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 7))) + } + .store(in: &disposeBag) + // name $authorName .sink { metaContent in diff --git a/MastodonSDK/Sources/MastodonUI/View/ImageView/AvatarImageView.swift b/MastodonSDK/Sources/MastodonUI/View/ImageView/AvatarImageView.swift index 047582e1ce..92f800443f 100644 --- a/MastodonSDK/Sources/MastodonUI/View/ImageView/AvatarImageView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/ImageView/AvatarImageView.swift @@ -11,7 +11,7 @@ import AlamofireImage public class AvatarImageView: FLAnimatedImageView { public var imageViewSize: CGSize? - public var configuration = Configuration(url: nil) + public var url: URL? = nil public var cornerConfiguration = CornerConfiguration() } @@ -55,35 +55,17 @@ extension AvatarImageView { public static let placeholder = UIImage.placeholder(color: .systemFill) - public struct Configuration { - public let url: URL? - - public init( - url: URL? - ) { - self.url = url - } - - public init( - image: UIImage - ) { - self.url = nil - } - } - - public func configure(configuration: Configuration) { + public func configure(with url: URL?) { prepareForReuse() - self.configuration = configuration - - guard let url = configuration.url else { - return - } + self.url = url + guard let url else { return } + switch url.pathExtension.lowercased() { case "gif": setImage( - url: configuration.url, + url: url, scaleToSize: imageViewSize ) default: