From 7a13b39c5111470220a76401e89a30af01821fb8 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 28 Apr 2021 20:36:10 +0800 Subject: [PATCH] feat: handle profile banner preview --- .../Protocol/AvatarConfigurableView.swift | 2 +- .../MediaPreviewViewController.swift | 2 +- .../MediaPreview/MediaPreviewViewModel.swift | 26 ++++++++++++++++ .../Header/View/ProfileHeaderView.swift | 12 +++++++ .../Scene/Profile/ProfileViewController.swift | 31 +++++++++++++++++++ ...wViewControllerAnimatedTransitioning.swift | 21 ++++++++++--- .../MediaPreviewTransitionItem.swift | 3 ++ .../MediaPreviewableViewController.swift | 2 ++ 8 files changed, 93 insertions(+), 6 deletions(-) diff --git a/Mastodon/Protocol/AvatarConfigurableView.swift b/Mastodon/Protocol/AvatarConfigurableView.swift index f2e954910..3d2dba802 100644 --- a/Mastodon/Protocol/AvatarConfigurableView.swift +++ b/Mastodon/Protocol/AvatarConfigurableView.swift @@ -164,7 +164,7 @@ struct AvatarConfigurableViewConfiguration { placeholderImage: UIImage? = nil, borderColor: UIColor? = nil, borderWidth: CGFloat? = nil, - keepImageCorner: Bool = true + keepImageCorner: Bool = false // default clip corner on image ) { self.avatarImageURL = avatarImageURL self.placeholderImage = placeholderImage diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift index 01d33853a..6feaaff49 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift @@ -106,7 +106,7 @@ extension MediaPreviewViewController { mosaicImageViewContainer.setImageViews(alpha: 1) mosaicImageViewContainer.setImageView(alpha: 0, index: index) } - case .profileAvatar: + case .profileAvatar, .profileBanner: break } } diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift index 6a0b749b6..1b0cc6def 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -52,6 +52,26 @@ final class MediaPreviewViewModel: NSObject { super.init() } + init(context: AppContext, meta: ProfileBannerImagePreviewMeta, pushTransitionItem: MediaPreviewTransitionItem) { + self.context = context + self.initialItem = .profileBanner(meta) + var viewControllers: [UIViewController] = [] + let managedObjectContext = self.context.managedObjectContext + managedObjectContext.performAndWait { + let account = managedObjectContext.object(with: meta.accountObjectID) as! MastodonUser + let avatarURL = account.headerImageURL() ?? 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() + } + init(context: AppContext, meta: ProfileAvatarImagePreviewMeta, pushTransitionItem: MediaPreviewTransitionItem) { self.context = context self.initialItem = .profileAvatar(meta) @@ -79,6 +99,7 @@ extension MediaPreviewViewModel { enum PreviewItem { case status(StatusImagePreviewMeta) case profileAvatar(ProfileAvatarImagePreviewMeta) + case profileBanner(ProfileBannerImagePreviewMeta) case local(LocalImagePreviewMeta) } @@ -93,6 +114,11 @@ extension MediaPreviewViewModel { let preloadThumbnailImage: UIImage? } + struct ProfileBannerImagePreviewMeta { + let accountObjectID: NSManagedObjectID + let preloadThumbnailImage: UIImage? + } + struct LocalImagePreviewMeta { let image: UIImage } diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 1f1f6b711..d5fcc5c47 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -12,6 +12,7 @@ import TwitterTextEditor protocol ProfileHeaderViewDelegate: AnyObject { func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView) + func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView) func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity) @@ -43,6 +44,7 @@ final class ProfileHeaderView: UIView { imageView.image = .placeholder(color: ProfileHeaderView.bannerImageViewPlaceholderColor) imageView.backgroundColor = ProfileHeaderView.bannerImageViewPlaceholderColor imageView.layer.masksToBounds = true + imageView.isUserInteractionEnabled = true // #if DEBUG // imageView.image = .placeholder(color: .red) // #endif @@ -338,6 +340,11 @@ extension ProfileHeaderView { let avatarImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer avatarImageView.addGestureRecognizer(avatarImageViewSingleTapGestureRecognizer) avatarImageViewSingleTapGestureRecognizer.addTarget(self, action: #selector(ProfileHeaderView.avatarImageViewDidPressed(_:))) + + let bannerImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer + bannerImageView.addGestureRecognizer(bannerImageViewSingleTapGestureRecognizer) + bannerImageViewSingleTapGestureRecognizer.addTarget(self, action: #selector(ProfileHeaderView.bannerImageViewDidPressed(_:))) + relationshipActionButton.addTarget(self, action: #selector(ProfileHeaderView.relationshipActionButtonDidPressed(_:)), for: .touchUpInside) configure(state: .normal) @@ -402,6 +409,11 @@ extension ProfileHeaderView { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) delegate?.profileHeaderView(self, avatarImageViewDidPressed: avatarImageView) } + + @objc private func bannerImageViewDidPressed(_ sender: UITapGestureRecognizer) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.profileHeaderView(self, bannerImageViewDidPressed: bannerImageView) + } } // MARK: - ActiveLabelDelegate diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index ff8c38edf..7df3bc235 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -679,6 +679,37 @@ extension ProfileViewController: ProfileHeaderViewDelegate { } } + func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView) { + guard let mastodonUser = viewModel.mastodonUser.value else { return } + guard let header = imageView.image else { return } + + let meta = MediaPreviewViewModel.ProfileBannerImagePreviewMeta( + accountObjectID: mastodonUser.objectID, + preloadThumbnailImage: header + ) + let pushTransitionItem = MediaPreviewTransitionItem( + source: .profileBanner(profileHeaderView), + previewableViewController: self + ) + pushTransitionItem.aspectRatio = header.size + pushTransitionItem.sourceImageView = imageView + pushTransitionItem.initialFrame = { + let initialFrame = imageView.superview!.convert(imageView.frame, to: nil) + assert(initialFrame != .zero) + return initialFrame + }() + pushTransitionItem.image = header + + 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) { diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index 1c54fadf0..a0bf523f9 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -204,7 +204,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { transitionItem.imageView = imageView transitionItem.snapshotTransitioning = snapshot transitionItem.initialFrame = snapshot.frame - transitionItem.targetFrame = targetFrame + transitionItem.targetFrame = targetFrame ?? snapshot.frame // disable interaction fromVC.pagingViewConttroller.isUserInteractionEnabled = false @@ -215,6 +215,12 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { self.transitionItem.snapshotRaw?.alpha = 0.0 animator.addAnimations { + switch self.transitionItem.source { + case .profileBanner: + self.transitionItem.snapshotTransitioning?.alpha = 0.4 + default: + break + } fromVC.closeButtonBackground.alpha = 0 fromVC.visualEffectView.effect = nil self.transitionItem.sourceImageViewCornerRadius.flatMap { self.transitionItem.snapshotTransitioning?.layer.cornerRadius = $0 } @@ -310,11 +316,18 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { itemAnimator.addAnimations { if toPosition == .end { - if let targetFrame = self.transitionItem.targetFrame { - self.transitionItem.snapshotTransitioning?.frame = targetFrame - } else { + switch self.transitionItem.source { + case .profileBanner where toPosition == .end: + // fade transition for banner self.transitionItem.snapshotTransitioning?.alpha = 0 + default: + if let targetFrame = self.transitionItem.targetFrame { + self.transitionItem.snapshotTransitioning?.frame = targetFrame + } else { + self.transitionItem.snapshotTransitioning?.alpha = 0 + } } + } else { if let initialFrame = self.transitionItem.initialFrame { self.transitionItem.snapshotTransitioning?.frame = initialFrame diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift b/Mastodon/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift index 48533cf38..47fdd215d 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift @@ -43,6 +43,7 @@ extension MediaPreviewTransitionItem { enum Source { case mosaic(MosaicImageViewContainer) case profileAvatar(ProfileHeaderView) + case profileBanner(ProfileHeaderView) func updateAppearance(position: UIViewAnimatingPosition, index: Int?) { let alpha: CGFloat = position == .end ? 1 : 0 @@ -55,6 +56,8 @@ extension MediaPreviewTransitionItem { } case .profileAvatar(let profileHeaderView): profileHeaderView.avatarImageView.alpha = alpha + case .profileBanner: + break // keep source } } } diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift b/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift index c7080b217..8029c09da 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift @@ -21,6 +21,8 @@ extension MediaPreviewableViewController { return imageView.superview!.convert(imageView.frame, to: nil) case .profileAvatar(let profileHeaderView): return profileHeaderView.avatarImageView.superview!.convert(profileHeaderView.avatarImageView.frame, to: nil) + case .profileBanner: + return nil // fallback to snapshot.frame } } }