feat: handle profile avatar preview

This commit is contained in:
CMK 2021-04-28 20:10:17 +08:00
parent 6f0b4354a7
commit acbbafb18f
10 changed files with 138 additions and 22 deletions

View File

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

View File

@ -106,6 +106,8 @@ extension MediaPreviewViewController {
mosaicImageViewContainer.setImageViews(alpha: 1)
mosaicImageViewContainer.setImageView(alpha: 0, index: index)
}
case .profileAvatar:
break
}
}
.store(in: &disposeBag)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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