From 531f71b77d226bcac2abab7a726e1dbf7b793f14 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 16 May 2022 19:42:03 +0800 Subject: [PATCH] feat: add familiar followers UI component for ProfileCard --- .../Discovery/DiscoverySection.swift | 17 +++- .../DiscoveryForYouViewModel+Diffable.swift | 3 +- .../ForYou/DiscoveryForYouViewModel.swift | 38 +++++++-- .../APIService/APIService+Recommend.swift | 33 +++---- ...stodon+API+Account+FamiliarFollowers.swift | 15 ++-- .../Mastodon+Entity+FamiliarFollowers.swift | 24 ++++++ .../MastodonSDK/Mastodon+Entity+Account.swift | 30 +++++++ ...FollowersDashboardView+Configuration.swift | 20 +++++ ...liarFollowersDashboardView+ViewModel.swift | 63 ++++++++++++++ .../FamiliarFollowersDashboardView.swift | 85 +++++++++++++++++++ .../Content/NotificationView+ViewModel.swift | 2 +- .../ProfileCardView+Configuration.swift | 1 + .../Content/ProfileCardView+ViewModel.swift | 16 ++++ .../View/Content/ProfileCardView.swift | 11 ++- .../View/ImageView/AvatarImageView.swift | 18 +++- ...ofileCardTableViewCell+Configuration.swift | 1 + 16 files changed, 342 insertions(+), 35 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+FamiliarFollowers.swift create mode 100644 MastodonSDK/Sources/MastodonUI/Extension/MastodonSDK/Mastodon+Entity+Account.swift create mode 100644 MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+Configuration.swift create mode 100644 MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift create mode 100644 MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView.swift diff --git a/Mastodon/Diffiable/Discovery/DiscoverySection.swift b/Mastodon/Diffiable/Discovery/DiscoverySection.swift index cab2eb82..94e07c71 100644 --- a/Mastodon/Diffiable/Discovery/DiscoverySection.swift +++ b/Mastodon/Diffiable/Discovery/DiscoverySection.swift @@ -8,6 +8,7 @@ import os.log import UIKit import MastodonUI +import MastodonSDK enum DiscoverySection: CaseIterable { // case posts @@ -22,9 +23,14 @@ extension DiscoverySection { class Configuration { weak var profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? + let familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher? - public init(profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? = nil) { + public init( + profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? = nil, + familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher? = nil + ) { self.profileCardTableViewCellDelegate = profileCardTableViewCellDelegate + self.familiarFollowers = familiarFollowers } } @@ -57,6 +63,15 @@ extension DiscoverySection { user: user, profileCardTableViewCellDelegate: configuration.profileCardTableViewCellDelegate ) + // bind familiarFollowers + if let familiarFollowers = configuration.familiarFollowers { + familiarFollowers + .map { array in array.first(where: { $0.id == user.id }) } + .assign(to: \.familiarFollowers, on: cell.profileCardView.viewModel) + .store(in: &cell.disposeBag) + } else { + cell.profileCardView.viewModel.familiarFollowers = nil + } } context.authenticationService.activeMastodonAuthentication .map { $0?.user } diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel+Diffable.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel+Diffable.swift index fe53cf22..f93b4c0b 100644 --- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel+Diffable.swift +++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel+Diffable.swift @@ -19,7 +19,8 @@ extension DiscoveryForYouViewModel { tableView: tableView, context: context, configuration: DiscoverySection.Configuration( - profileCardTableViewCellDelegate: profileCardTableViewCellDelegate + profileCardTableViewCellDelegate: profileCardTableViewCellDelegate, + familiarFollowers: $familiarFollowers ) ) diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift index 7be4aeba..a31022a7 100644 --- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift +++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift @@ -20,6 +20,9 @@ final class DiscoveryForYouViewModel { // input let context: AppContext let userFetchedResultsController: UserFetchedResultsController + + @MainActor + @Published var familiarFollowers: [Mastodon.Entity.FamiliarFollowers] = [] @Published var isFetching = false // output @@ -48,12 +51,35 @@ final class DiscoveryForYouViewModel { } extension DiscoveryForYouViewModel { + + @MainActor func fetch() async throws { guard !isFetching else { return } isFetching = true defer { isFetching = false } - - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } + + guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { + throw APIService.APIError.implicit(.badRequest) + } + + do { + let userIDs = try await fetchSuggestionAccounts() + + let _familiarFollowersResponse = try? await context.apiService.familiarFollowers( + query: .init(ids: userIDs), + authenticationBox: authenticationBox + ) + familiarFollowers = _familiarFollowersResponse?.value ?? [] + userFetchedResultsController.userIDs = userIDs + } catch { + // do nothing + } + } + + private func fetchSuggestionAccounts() async throws -> [Mastodon.Entity.Account.ID] { + guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { + throw APIService.APIError.implicit(.badRequest) + } do { let response = try await context.apiService.suggestionAccountV2( @@ -61,15 +87,15 @@ extension DiscoveryForYouViewModel { authenticationBox: authenticationBox ) let userIDs = response.value.map { $0.account.id } - userFetchedResultsController.userIDs = userIDs + return userIDs } catch { // fallback V1 - let response2 = try await context.apiService.suggestionAccount( + let response = try await context.apiService.suggestionAccount( query: nil, authenticationBox: authenticationBox ) - let userIDs = response2.value.map { $0.id } - userFetchedResultsController.userIDs = userIDs + let userIDs = response.value.map { $0.id } + return userIDs } } } diff --git a/Mastodon/Service/APIService/APIService+Recommend.swift b/Mastodon/Service/APIService/APIService+Recommend.swift index 87471250..f88d8230 100644 --- a/Mastodon/Service/APIService/APIService+Recommend.swift +++ b/Mastodon/Service/APIService/APIService+Recommend.swift @@ -80,7 +80,7 @@ extension APIService { func familiarFollowers( query: Mastodon.API.Account.FamiliarFollowersQuery, authenticationBox: MastodonAuthenticationBox - ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> { + ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.FamiliarFollowers]> { let response = try await Mastodon.API.Account.familiarFollowers( session: session, domain: authenticationBox.domain, @@ -88,20 +88,23 @@ extension APIService { authorization: authenticationBox.userAuthorization ).singleOutput() -// let managedObjectContext = backgroundManagedObjectContext -// try await managedObjectContext.performChanges { -// for entity in response.value { -// _ = Persistence.MastodonUser.createOrMerge( -// in: managedObjectContext, -// context: Persistence.MastodonUser.PersistContext( -// domain: authenticationBox.domain, -// entity: entity.account, -// cache: nil, -// networkDate: response.networkDate -// ) -// ) -// } // end for … in -// } + let managedObjectContext = backgroundManagedObjectContext + try await managedObjectContext.performChanges { + for entity in response.value { + for account in entity.accounts { + _ = Persistence.MastodonUser.createOrMerge( + in: managedObjectContext, + context: Persistence.MastodonUser.PersistContext( + domain: authenticationBox.domain, + entity: account, + cache: nil, + networkDate: response.networkDate + ) + ) + + } // end for account in + } // end for entity in + } return response } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FamiliarFollowers.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FamiliarFollowers.swift index a1f0b4f6..97d7b461 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FamiliarFollowers.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FamiliarFollowers.swift @@ -36,7 +36,7 @@ extension Mastodon.API.Account { domain: String, query: FamiliarFollowersQuery, authorization: Mastodon.API.OAuth.Authorization - ) -> AnyPublisher, Error> { + ) -> AnyPublisher, Error> { let request = Mastodon.API.get( url: familiarFollowersEndpointURL(domain: domain), query: query, @@ -44,24 +44,23 @@ extension Mastodon.API.Account { ) return session.dataTaskPublisher(for: request) .tryMap { data, response in - let value = try Mastodon.API.decode(type: [Mastodon.Entity.Account].self, from: data, response: response) + let value = try Mastodon.API.decode(type: [Mastodon.Entity.FamiliarFollowers].self, from: data, response: response) return Mastodon.Response.Content(value: value, response: response) } .eraseToAnyPublisher() } public struct FamiliarFollowersQuery: GetQuery { - public let accounts: [Mastodon.Entity.Account.ID] + public let ids: [Mastodon.Entity.Account.ID] - public init(accounts: [Mastodon.Entity.Account.ID]) { - self.accounts = accounts + public init(ids: [Mastodon.Entity.Account.ID]) { + self.ids = ids } var queryItems: [URLQueryItem]? { var items: [URLQueryItem] = [] - let accountsValue = accounts.joined(separator: ",") - if !accountsValue.isEmpty { - items.append(URLQueryItem(name: "accounts", value: accountsValue)) + for id in ids { + items.append(URLQueryItem(name: "id[]", value: id)) } guard !items.isEmpty else { return nil } return items diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+FamiliarFollowers.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+FamiliarFollowers.swift new file mode 100644 index 00000000..ce411619 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+FamiliarFollowers.swift @@ -0,0 +1,24 @@ +// +// Mastodon+Entity+FamiliarFollowers.swift +// +// +// Created by MainasuK on 2022-5-16. +// + +import Foundation + +extension Mastodon.Entity { + + /// FamiliarFollowers + /// + /// - Since: 3.5.2 + /// - Version: 3.5.2 + /// # Last Update + /// 2022/5/16 + /// # Reference + /// [Document](TBD) + public class FamiliarFollowers: Codable { + public let id: Mastodon.Entity.Account.ID + public let accounts: [Mastodon.Entity.Account] + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Extension/MastodonSDK/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonUI/Extension/MastodonSDK/Mastodon+Entity+Account.swift new file mode 100644 index 00000000..2441ebfd --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Extension/MastodonSDK/Mastodon+Entity+Account.swift @@ -0,0 +1,30 @@ +// +// Mastodon+Entity+Account.swift +// +// +// Created by MainasuK on 2022-5-16. +// + +import Foundation +import MastodonSDK + +extension Mastodon.Entity.Account { + public var displayNameWithFallback: String { + if displayName.isEmpty { + return username + } else { + return displayName + } + } +} + +extension Mastodon.Entity.Account { + public func avatarImageURL() -> URL? { + let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar + return URL(string: string) + } + + public func avatarImageURLWithFallback(domain: String) -> URL { + return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")! + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+Configuration.swift new file mode 100644 index 00000000..63c962b7 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+Configuration.swift @@ -0,0 +1,20 @@ +// +// FamiliarFollowersDashboardView+Configuration.swift +// +// +// Created by MainasuK on 2022-5-16. +// + +import UIKit +import MastodonSDK + +extension FamiliarFollowersDashboardView { + public func configure(familiarFollowers: Mastodon.Entity.FamiliarFollowers?) { + assert(Thread.isMainThread) + + let accounts = familiarFollowers?.accounts.prefix(4) ?? [] + + viewModel.avatarURLs = accounts.map { $0.avatarImageURL() } + viewModel.names = accounts.map { $0.displayNameWithFallback } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift new file mode 100644 index 00000000..4ab9200c --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+ViewModel.swift @@ -0,0 +1,63 @@ +// +// FamiliarFollowersDashboardView+ViewModel.swift +// +// +// Created by MainasuK on 2022-5-16. +// + +import os.log +import UIKit +import Combine + +extension FamiliarFollowersDashboardView { + public final class ViewModel: ObservableObject { + public var disposeBag = Set() + + let logger = Logger(subsystem: "FamiliarFollowersDashboardView", category: "ViewModel") + + @Published var avatarURLs: [URL?] = [] + @Published var names: [String] = [] + @Published var backgroundColor: UIColor? + } +} + +extension FamiliarFollowersDashboardView.ViewModel { + func bind(view: FamiliarFollowersDashboardView) { + Publishers.CombineLatest( + $avatarURLs, + $backgroundColor + ) + .sink { avatarURLs, backgroundColor in + view.avatarContainerView.subviews.forEach { $0.removeFromSuperview() } + for (i, avatarURL) in avatarURLs.enumerated() { + let avatarButton = AvatarButton() + let origin = CGPoint(x: 20 * i, y: 0) + let size = CGSize(width: 32, height: 32) + avatarButton.size = size + avatarButton.frame = CGRect(origin: origin, size: size) + view.avatarContainerView.addSubview(avatarButton) + avatarButton.avatarImageView.configure(configuration: .init(url: avatarURL)) + avatarButton.avatarImageView.configure( + cornerConfiguration: .init( + corner: .fixed(radius: 7), + border: .init( + color: backgroundColor ?? .clear, + width: 1 + ) + ) + ) + } + + view.avatarContainerViewWidthLayoutConstraint.constant = CGFloat(12 + 20 * avatarURLs.count) + } + .store(in: &disposeBag) + + $names + .sink { names in + // TODO: i18n + let description = "Followed by" + names.joined(separator: ", ") + view.descriptionLabel.text = description + } + .store(in: &disposeBag) + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView.swift new file mode 100644 index 00000000..8bd14220 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView.swift @@ -0,0 +1,85 @@ +// +// FamiliarFollowersDashboardView.swift +// +// +// Created by MainasuK on 2022-5-16. +// + +import UIKit +import MastodonAsset + +public final class FamiliarFollowersDashboardView: UIView { + + let avatarContainerView = UIView() + var avatarContainerViewWidthLayoutConstraint: NSLayoutConstraint! + + let descriptionLabel: UILabel = { + let label = UILabel() + label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 13, weight: .regular)) + label.text = "Followed by Pixelflowers, Lee’s Food, and 4 other mutuals" + label.textColor = Asset.Colors.Label.secondary.color + label.numberOfLines = 0 + return label + }() + + public private(set) lazy var viewModel: ViewModel = { + let viewModel = ViewModel() + viewModel.bind(view: self) + return viewModel + }() + + public func prepareForReuse() { + + } + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension FamiliarFollowersDashboardView { + + private func _init() { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = 8 + + stackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(stackView) + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: topAnchor), + stackView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + + avatarContainerView.translatesAutoresizingMaskIntoConstraints = false + stackView.addArrangedSubview(avatarContainerView) + avatarContainerViewWidthLayoutConstraint = avatarContainerView.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1) + NSLayoutConstraint.activate([ + avatarContainerViewWidthLayoutConstraint, + avatarContainerView.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1) + ]) + stackView.addArrangedSubview(descriptionLabel) + } + +} + + +#if DEBUG +import SwiftUI +struct FamiliarFollowersDashboardView_Preview: PreviewProvider { + static var previews: some View { + UIViewPreview { + FamiliarFollowersDashboardView() + } + } +} +#endif diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift index 2151b55b..eec57cb5 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift @@ -18,7 +18,7 @@ extension NotificationView { public final class ViewModel: ObservableObject { public var disposeBag = Set() - let logger = Logger(subsystem: "StatusView", category: "ViewModel") + let logger = Logger(subsystem: "NotificationView", category: "ViewModel") @Published public var userIdentifier: UserIdentifier? // me diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+Configuration.swift index 3964099d..350c4373 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+Configuration.swift @@ -10,6 +10,7 @@ import Combine import CoreDataStack import Meta import MastodonMeta +import MastodonSDK extension ProfileCardView { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+ViewModel.swift index 5b6c4c59..0003e82b 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+ViewModel.swift @@ -13,6 +13,7 @@ import AlamofireImage import CoreDataStack import MastodonLocalization import MastodonAsset +import MastodonSDK extension ProfileCardView { public class ViewModel: ObservableObject { @@ -44,6 +45,8 @@ extension ProfileCardView { @Published public var groupedAccessibilityLabel = "" + @Published public var familiarFollowers: Mastodon.Entity.FamiliarFollowers? + init() { backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor Publishers.CombineLatest( @@ -77,6 +80,7 @@ extension ProfileCardView.ViewModel { bindBio(view: view) bindRelationship(view: view) bindDashboard(view: view) + bindFamiliarFollowers(view: view) bindAccessibility(view: view) } @@ -189,6 +193,18 @@ extension ProfileCardView.ViewModel { .store(in: &disposeBag) } + private func bindFamiliarFollowers(view: ProfileCardView) { + $familiarFollowers + .sink { familiarFollowers in + view.familiarFollowersDashboardViewAdaptiveMarginContainerView.isHidden = familiarFollowers.flatMap { $0.accounts.isEmpty } ?? true + view.familiarFollowersDashboardView.configure(familiarFollowers: familiarFollowers) + } + .store(in: &disposeBag) + $backgroundColor + .assign(to: \.backgroundColor, on: view.familiarFollowersDashboardView.viewModel) + .store(in: &disposeBag) + } + private func bindAccessibility(view: ProfileCardView) { let authorAccessibilityLabel = Publishers.CombineLatest( $authorName, diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView.swift index 07f44150..c0d74320 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView.swift @@ -94,6 +94,9 @@ public final class ProfileCardView: UIView { return button }() + let familiarFollowersDashboardViewAdaptiveMarginContainerView = AdaptiveMarginContainerView() + let familiarFollowersDashboardView = FamiliarFollowersDashboardView() + public private(set) lazy var viewModel: ViewModel = { let viewModel = ViewModel() viewModel.bind(view: self) @@ -126,7 +129,7 @@ extension ProfileCardView { bioMetaText.textView.isUserInteractionEnabled = false statusDashboardView.isUserInteractionEnabled = false - // container: V - [ bannerContainer | authorContainer | bioMetaText | infoContainer ] + // container: V - [ bannerContainer | authorContainer | bioMetaText | infoContainer | familiarFollowersDashboardView ] container.axis = .vertical container.spacing = 8 container.translatesAutoresizingMaskIntoConstraints = false @@ -211,7 +214,7 @@ extension ProfileCardView { container.addArrangedSubview(bioMetaTextAdaptiveMarginContainerView) container.setCustomSpacing(16, after: bioMetaTextAdaptiveMarginContainerView) - // infoContainer: H - [ statusDashboardView | (spacer) | relationshipActionButton ] + // infoContainer: H - [ statusDashboardView | (spacer) | relationshipActionButton] infoContainer.axis = .horizontal infoContainer.spacing = 8 infoContainerAdaptiveMarginContainerView.contentView = infoContainer @@ -237,6 +240,10 @@ extension ProfileCardView { relationshipActionButtonShadowContainer.widthAnchor.constraint(greaterThanOrEqualToConstant: ProfileCardView.friendshipActionButtonSize.width).priority(.required - 1), relationshipActionButtonShadowContainer.heightAnchor.constraint(equalToConstant: ProfileCardView.friendshipActionButtonSize.height).priority(.required - 1), ]) + + familiarFollowersDashboardViewAdaptiveMarginContainerView.contentView = familiarFollowersDashboardView + familiarFollowersDashboardViewAdaptiveMarginContainerView.margin = ProfileCardView.contentMargin + container.addArrangedSubview(familiarFollowersDashboardViewAdaptiveMarginContainerView) let bottomPadding = UIView() bottomPadding.translatesAutoresizingMaskIntoConstraints = false diff --git a/MastodonSDK/Sources/MastodonUI/View/ImageView/AvatarImageView.swift b/MastodonSDK/Sources/MastodonUI/View/ImageView/AvatarImageView.swift index c0204bc6..c2fdbc92 100644 --- a/MastodonSDK/Sources/MastodonUI/View/ImageView/AvatarImageView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/ImageView/AvatarImageView.swift @@ -44,6 +44,11 @@ extension AvatarImageView { } } + private func setup(border: CornerConfiguration.Border?) { + layer.borderColor = border?.color.cgColor + layer.borderWidth = border?.width ?? .zero + } + } extension AvatarImageView { @@ -107,9 +112,14 @@ extension AvatarImageView { extension AvatarImageView { public struct CornerConfiguration { public let corner: Corner + public let border: Border? - public init(corner: Corner = .circle) { + public init( + corner: Corner = .circle, + border: Border? = nil + ) { self.corner = corner + self.border = border } public enum Corner { @@ -117,10 +127,16 @@ extension AvatarImageView { case fixed(radius: CGFloat) case scale(ratio: Int = 4) // width / ratio } + + public struct Border { + public let color: UIColor + public let width: CGFloat + } } public func configure(cornerConfiguration: CornerConfiguration) { self.cornerConfiguration = cornerConfiguration setup(corner: cornerConfiguration.corner) + setup(border: cornerConfiguration.border) } } diff --git a/MastodonSDK/Sources/MastodonUI/View/TableViewCell/ProfileCardTableViewCell+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/ProfileCardTableViewCell+Configuration.swift index 061af0f4..1274b796 100644 --- a/MastodonSDK/Sources/MastodonUI/View/TableViewCell/ProfileCardTableViewCell+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/ProfileCardTableViewCell+Configuration.swift @@ -7,6 +7,7 @@ import UIKit import CoreDataStack +import MastodonSDK extension ProfileCardTableViewCell {