feat: handle profile avatar preview
This commit is contained in:
parent
6f0b4354a7
commit
acbbafb18f
|
@ -51,7 +51,10 @@ extension AvatarConfigurableView {
|
|||
avatarConfigurableView(self, didFinishConfiguration: configuration)
|
||||
}
|
||||
|
||||
let filter = ScaledToSizeWithRoundedCornersFilter(size: Self.configurableAvatarImageSize, radius: Self.configurableAvatarImageCornerRadius)
|
||||
let filter = ScaledToSizeWithRoundedCornersFilter(
|
||||
size: Self.configurableAvatarImageSize,
|
||||
radius: configuration.keepImageCorner ? 0 : Self.configurableAvatarImageCornerRadius
|
||||
)
|
||||
|
||||
// set placeholder if no asset
|
||||
guard let avatarImageURL = configuration.avatarImageURL else {
|
||||
|
@ -91,6 +94,12 @@ extension AvatarConfigurableView {
|
|||
runImageTransitionIfCached: false,
|
||||
completion: nil
|
||||
)
|
||||
|
||||
if Self.configurableAvatarImageCornerRadius > 0, configuration.keepImageCorner {
|
||||
configurableAvatarImageView?.layer.masksToBounds = true
|
||||
configurableAvatarImageView?.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
|
||||
configurableAvatarImageView?.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous :.circular
|
||||
}
|
||||
}
|
||||
|
||||
configureLayerBorder(view: avatarImageView, configuration: configuration)
|
||||
|
@ -148,16 +157,20 @@ struct AvatarConfigurableViewConfiguration {
|
|||
let borderColor: UIColor?
|
||||
let borderWidth: CGFloat?
|
||||
|
||||
let keepImageCorner: Bool
|
||||
|
||||
init(
|
||||
avatarImageURL: URL?,
|
||||
placeholderImage: UIImage? = nil,
|
||||
borderColor: UIColor? = nil,
|
||||
borderWidth: CGFloat? = nil
|
||||
borderWidth: CGFloat? = nil,
|
||||
keepImageCorner: Bool = true
|
||||
) {
|
||||
self.avatarImageURL = avatarImageURL
|
||||
self.placeholderImage = placeholderImage
|
||||
self.borderColor = borderColor
|
||||
self.borderWidth = borderWidth
|
||||
self.keepImageCorner = keepImageCorner
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -106,6 +106,8 @@ extension MediaPreviewViewController {
|
|||
mosaicImageViewContainer.setImageViews(alpha: 1)
|
||||
mosaicImageViewContainer.setImageView(alpha: 0, index: index)
|
||||
}
|
||||
case .profileAvatar:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
|
|
@ -36,7 +36,7 @@ final class MediaPreviewViewModel: NSObject {
|
|||
switch entity.type {
|
||||
case .image:
|
||||
guard let url = URL(string: entity.url) else { continue }
|
||||
let meta = MediaPreviewImageViewModel.StatusImagePreviewMeta(url: url, thumbnail: thumbnail)
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: url, thumbnail: thumbnail)
|
||||
let mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
|
||||
let mediaPreviewImageViewController = MediaPreviewImageViewController()
|
||||
mediaPreviewImageViewController.viewModel = mediaPreviewImageModel
|
||||
|
@ -52,12 +52,33 @@ final class MediaPreviewViewModel: NSObject {
|
|||
super.init()
|
||||
}
|
||||
|
||||
init(context: AppContext, meta: ProfileAvatarImagePreviewMeta, pushTransitionItem: MediaPreviewTransitionItem) {
|
||||
self.context = context
|
||||
self.initialItem = .profileAvatar(meta)
|
||||
var viewControllers: [UIViewController] = []
|
||||
let managedObjectContext = self.context.managedObjectContext
|
||||
managedObjectContext.performAndWait {
|
||||
let account = managedObjectContext.object(with: meta.accountObjectID) as! MastodonUser
|
||||
let avatarURL = account.avatarImageURL() ?? URL(string: "https://example.com")! // assert URL exist
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: avatarURL, thumbnail: meta.preloadThumbnailImage)
|
||||
let mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
|
||||
let mediaPreviewImageViewController = MediaPreviewImageViewController()
|
||||
mediaPreviewImageViewController.viewModel = mediaPreviewImageModel
|
||||
viewControllers.append(mediaPreviewImageViewController)
|
||||
}
|
||||
self.viewControllers = viewControllers
|
||||
self.currentPage = CurrentValueSubject(0)
|
||||
self.pushTransitionItem = pushTransitionItem
|
||||
super.init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MediaPreviewViewModel {
|
||||
|
||||
enum PreviewItem {
|
||||
case status(StatusImagePreviewMeta)
|
||||
case profileAvatar(ProfileAvatarImagePreviewMeta)
|
||||
case local(LocalImagePreviewMeta)
|
||||
}
|
||||
|
||||
|
@ -67,6 +88,11 @@ extension MediaPreviewViewModel {
|
|||
let preloadThumbnailImages: [UIImage?]
|
||||
}
|
||||
|
||||
struct ProfileAvatarImagePreviewMeta {
|
||||
let accountObjectID: NSManagedObjectID
|
||||
let preloadThumbnailImage: UIImage?
|
||||
}
|
||||
|
||||
struct LocalImagePreviewMeta {
|
||||
let image: UIImage
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ class MediaPreviewImageViewModel {
|
|||
// input
|
||||
let item: ImagePreviewItem
|
||||
|
||||
init(meta: StatusImagePreviewMeta) {
|
||||
init(meta: RemoteImagePreviewMeta) {
|
||||
self.item = .status(meta)
|
||||
}
|
||||
|
||||
|
@ -25,11 +25,11 @@ class MediaPreviewImageViewModel {
|
|||
|
||||
extension MediaPreviewImageViewModel {
|
||||
enum ImagePreviewItem {
|
||||
case status(StatusImagePreviewMeta)
|
||||
case status(RemoteImagePreviewMeta)
|
||||
case local(LocalImagePreviewMeta)
|
||||
}
|
||||
|
||||
struct StatusImagePreviewMeta {
|
||||
struct RemoteImagePreviewMeta {
|
||||
let url: URL
|
||||
let thumbnail: UIImage?
|
||||
}
|
||||
|
|
|
@ -150,8 +150,7 @@ extension ProfileHeaderViewController {
|
|||
with: AvatarConfigurableViewConfiguration(
|
||||
avatarImageURL: image == nil ? url : nil, // set only when image empty
|
||||
placeholderImage: image,
|
||||
borderColor: .white,
|
||||
borderWidth: 2
|
||||
keepImageCorner: true // fit preview transitioning
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ import UIKit
|
|||
import ActiveLabel
|
||||
import TwitterTextEditor
|
||||
|
||||
protocol ProfileHeaderViewDelegate: class {
|
||||
protocol ProfileHeaderViewDelegate: AnyObject {
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView)
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton)
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity)
|
||||
|
||||
|
@ -23,6 +24,8 @@ final class ProfileHeaderView: UIView {
|
|||
|
||||
static let avatarImageViewSize = CGSize(width: 56, height: 56)
|
||||
static let avatarImageViewCornerRadius: CGFloat = 6
|
||||
static let avatarImageViewBorderColor = UIColor.white
|
||||
static let avatarImageViewBorderWidth: CGFloat = 2
|
||||
static let friendshipActionButtonSize = CGSize(width: 108, height: 34)
|
||||
static let bannerImageViewPlaceholderColor = UIColor.systemGray
|
||||
|
||||
|
@ -51,6 +54,16 @@ final class ProfileHeaderView: UIView {
|
|||
return overlayView
|
||||
}()
|
||||
|
||||
let avatarImageViewBackgroundView: UIView = {
|
||||
let view = UIView()
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.cornerRadius = ProfileHeaderView.avatarImageViewCornerRadius
|
||||
view.layer.cornerCurve = .continuous
|
||||
view.layer.borderColor = ProfileHeaderView.avatarImageViewBorderColor.cgColor
|
||||
view.layer.borderWidth = ProfileHeaderView.avatarImageViewBorderWidth
|
||||
return view
|
||||
}()
|
||||
|
||||
let avatarImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
let placeholderImage = UIImage
|
||||
|
@ -188,6 +201,15 @@ extension ProfileHeaderView {
|
|||
avatarImageView.heightAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.height).priority(.required - 1),
|
||||
])
|
||||
|
||||
avatarImageViewBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
bannerContainerView.insertSubview(avatarImageViewBackgroundView, belowSubview: avatarImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
avatarImageView.topAnchor.constraint(equalTo: avatarImageViewBackgroundView.topAnchor, constant: ProfileHeaderView.avatarImageViewBorderWidth),
|
||||
avatarImageView.leadingAnchor.constraint(equalTo: avatarImageViewBackgroundView.leadingAnchor, constant: ProfileHeaderView.avatarImageViewBorderWidth),
|
||||
avatarImageViewBackgroundView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: ProfileHeaderView.avatarImageViewBorderWidth),
|
||||
avatarImageViewBackgroundView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: ProfileHeaderView.avatarImageViewBorderWidth),
|
||||
])
|
||||
|
||||
editAvatarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarImageView.addSubview(editAvatarBackgroundView)
|
||||
NSLayoutConstraint.activate([
|
||||
|
@ -313,6 +335,9 @@ extension ProfileHeaderView {
|
|||
|
||||
bioActiveLabel.delegate = self
|
||||
|
||||
let avatarImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
avatarImageView.addGestureRecognizer(avatarImageViewSingleTapGestureRecognizer)
|
||||
avatarImageViewSingleTapGestureRecognizer.addTarget(self, action: #selector(ProfileHeaderView.avatarImageViewDidPressed(_:)))
|
||||
relationshipActionButton.addTarget(self, action: #selector(ProfileHeaderView.relationshipActionButtonDidPressed(_:)), for: .touchUpInside)
|
||||
|
||||
configure(state: .normal)
|
||||
|
@ -372,6 +397,11 @@ extension ProfileHeaderView {
|
|||
assert(sender === relationshipActionButton)
|
||||
delegate?.profileHeaderView(self, relationshipButtonDidPressed: relationshipActionButton)
|
||||
}
|
||||
|
||||
@objc private func avatarImageViewDidPressed(_ sender: UITapGestureRecognizer) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.profileHeaderView(self, avatarImageViewDidPressed: avatarImageView)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ActiveLabelDelegate
|
||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
|||
import Combine
|
||||
import ActiveLabel
|
||||
|
||||
final class ProfileViewController: UIViewController, NeedsDependency {
|
||||
final class ProfileViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
@ -18,6 +18,8 @@ final class ProfileViewController: UIViewController, NeedsDependency {
|
|||
var disposeBag = Set<AnyCancellable>()
|
||||
var viewModel: ProfileViewModel!
|
||||
|
||||
let mediaPreviewTransitionController = MediaPreviewTransitionController()
|
||||
|
||||
private(set) lazy var cancelEditingBarButtonItem: UIBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ProfileViewController.cancelEditingBarButtonItemPressed(_:)))
|
||||
barButtonItem.tintColor = .white
|
||||
|
@ -645,6 +647,38 @@ extension ProfileViewController: ProfilePagingViewControllerDelegate {
|
|||
// MARK: - ProfileHeaderViewDelegate
|
||||
extension ProfileViewController: ProfileHeaderViewDelegate {
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView) {
|
||||
guard let mastodonUser = viewModel.mastodonUser.value else { return }
|
||||
guard let avatar = imageView.image else { return }
|
||||
|
||||
let meta = MediaPreviewViewModel.ProfileAvatarImagePreviewMeta(
|
||||
accountObjectID: mastodonUser.objectID,
|
||||
preloadThumbnailImage: avatar
|
||||
)
|
||||
let pushTransitionItem = MediaPreviewTransitionItem(
|
||||
source: .profileAvatar(profileHeaderView),
|
||||
previewableViewController: self
|
||||
)
|
||||
pushTransitionItem.aspectRatio = CGSize(width: 100, height: 100)
|
||||
pushTransitionItem.sourceImageView = imageView
|
||||
pushTransitionItem.sourceImageViewCornerRadius = ProfileHeaderView.avatarImageViewCornerRadius
|
||||
pushTransitionItem.initialFrame = {
|
||||
let initialFrame = imageView.superview!.convert(imageView.frame, to: nil)
|
||||
assert(initialFrame != .zero)
|
||||
return initialFrame
|
||||
}()
|
||||
pushTransitionItem.image = avatar
|
||||
|
||||
let mediaPreviewViewModel = MediaPreviewViewModel(
|
||||
context: context,
|
||||
meta: meta,
|
||||
pushTransitionItem: pushTransitionItem
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
self.coordinator.present(scene: .mediaPreview(viewModel: mediaPreviewViewModel), from: self, transition: .custom(transitioningDelegate: self.mediaPreviewTransitionController))
|
||||
}
|
||||
}
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) {
|
||||
let relationshipActionSet = viewModel.relationshipActionOptionSet.value
|
||||
if relationshipActionSet.contains(.edit) {
|
||||
|
|
|
@ -58,10 +58,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
// set to image hidden
|
||||
toVC.pagingViewConttroller.view.alpha = 0
|
||||
// set from image hidden. update hidden when paging. seealso: `MediaPreviewViewController`
|
||||
switch transitionItem.source {
|
||||
case .mosaic(let mosaicImageViewContainer):
|
||||
mosaicImageViewContainer.setImageView(alpha: 0, index: toVC.viewModel.currentPage.value)
|
||||
}
|
||||
transitionItem.source.updateAppearance(position: .start, index: toVC.viewModel.currentPage.value)
|
||||
|
||||
// Set transition image view
|
||||
assert(transitionItem.initialFrame != nil)
|
||||
|
@ -143,16 +140,14 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
} else {
|
||||
fromView.alpha = 0
|
||||
}
|
||||
self.transitionItem.sourceImageViewCornerRadius.flatMap { self.transitionItem.snapshotTransitioning?.layer.cornerRadius = $0 }
|
||||
fromVC.closeButtonBackground.alpha = 0
|
||||
fromVC.visualEffectView.effect = nil
|
||||
}
|
||||
|
||||
animator.addCompletion { position in
|
||||
self.transitionItem.snapshotTransitioning?.removeFromSuperview()
|
||||
switch self.transitionItem.source {
|
||||
case .mosaic(let mosaicImageViewContainer):
|
||||
mosaicImageViewContainer.setImageViews(alpha: 1)
|
||||
}
|
||||
self.transitionItem.source.updateAppearance(position: position, index: nil)
|
||||
transitionContext.completeTransition(position == .end)
|
||||
}
|
||||
|
||||
|
@ -222,6 +217,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
animator.addAnimations {
|
||||
fromVC.closeButtonBackground.alpha = 0
|
||||
fromVC.visualEffectView.effect = nil
|
||||
self.transitionItem.sourceImageViewCornerRadius.flatMap { self.transitionItem.snapshotTransitioning?.layer.cornerRadius = $0 }
|
||||
}
|
||||
|
||||
animator.addCompletion { position in
|
||||
|
@ -231,10 +227,8 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
self.transitionItem.snapshotRaw?.alpha = position == .start ? 1.0 : 0.0
|
||||
self.transitionItem.snapshotTransitioning?.removeFromSuperview()
|
||||
if position == .end {
|
||||
switch self.transitionItem.source {
|
||||
case .mosaic(let mosaicImageViewContainer):
|
||||
mosaicImageViewContainer.setImageViews(alpha: 1)
|
||||
}
|
||||
// reset appearance
|
||||
self.transitionItem.source.updateAppearance(position: position, index: nil)
|
||||
}
|
||||
fromVC.visualEffectView.effect = position == .end ? nil : blurEffect
|
||||
transitionContext.completeTransition(position == .end)
|
||||
|
|
|
@ -20,6 +20,7 @@ class MediaPreviewTransitionItem: Identifiable {
|
|||
var aspectRatio: CGSize?
|
||||
var initialFrame: CGRect? = nil
|
||||
var sourceImageView: UIImageView?
|
||||
var sourceImageViewCornerRadius: CGFloat?
|
||||
|
||||
// target
|
||||
var targetFrame: CGRect? = nil
|
||||
|
@ -41,5 +42,20 @@ class MediaPreviewTransitionItem: Identifiable {
|
|||
extension MediaPreviewTransitionItem {
|
||||
enum Source {
|
||||
case mosaic(MosaicImageViewContainer)
|
||||
case profileAvatar(ProfileHeaderView)
|
||||
|
||||
func updateAppearance(position: UIViewAnimatingPosition, index: Int?) {
|
||||
let alpha: CGFloat = position == .end ? 1 : 0
|
||||
switch self {
|
||||
case .mosaic(let mosaicImageViewContainer):
|
||||
if let index = index {
|
||||
mosaicImageViewContainer.setImageView(alpha: 0, index: index)
|
||||
} else {
|
||||
mosaicImageViewContainer.setImageViews(alpha: alpha)
|
||||
}
|
||||
case .profileAvatar(let profileHeaderView):
|
||||
profileHeaderView.avatarImageView.alpha = alpha
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ extension MediaPreviewableViewController {
|
|||
guard index < mosaicImageViewContainer.imageViews.count else { return nil }
|
||||
let imageView = mosaicImageViewContainer.imageViews[index]
|
||||
return imageView.superview!.convert(imageView.frame, to: nil)
|
||||
case .profileAvatar(let profileHeaderView):
|
||||
return profileHeaderView.avatarImageView.superview!.convert(profileHeaderView.avatarImageView.frame, to: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue