Basic banner editing support

This commit is contained in:
Jed Fox 2022-11-16 22:49:49 -05:00
parent c2bb14eaab
commit 5d91bcf8b5
No known key found for this signature in database
GPG Key ID: 0B61D18EA54B47E1
5 changed files with 62 additions and 14 deletions

View File

@ -61,6 +61,7 @@ final class ProfileHeaderViewController: UIViewController, NeedsDependency, Medi
// private var isAdjustBannerImageViewForSafeAreaInset = false // private var isAdjustBannerImageViewForSafeAreaInset = false
private var containerSafeAreaInset: UIEdgeInsets = .zero private var containerSafeAreaInset: UIEdgeInsets = .zero
private var currentImageType = ImageType.avatar
private(set) lazy var imagePicker: PHPickerViewController = { private(set) lazy var imagePicker: PHPickerViewController = {
var configuration = PHPickerConfiguration() var configuration = PHPickerConfiguration()
configuration.filter = .images configuration.filter = .images
@ -125,7 +126,9 @@ extension ProfileHeaderViewController {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
profileHeaderView.editAvatarButtonOverlayIndicatorView.menu = createAvatarContextMenu() profileHeaderView.editBannerButton.menu = createImageContextMenu(.banner)
profileHeaderView.editBannerButton.showsMenuAsPrimaryAction = true
profileHeaderView.editAvatarButtonOverlayIndicatorView.menu = createImageContextMenu(.avatar)
profileHeaderView.editAvatarButtonOverlayIndicatorView.showsMenuAsPrimaryAction = true profileHeaderView.editAvatarButtonOverlayIndicatorView.showsMenuAsPrimaryAction = true
profileHeaderView.delegate = self profileHeaderView.delegate = self
@ -173,7 +176,7 @@ extension ProfileHeaderViewController {
profileHeaderView.viewModel.viewDidAppear.send() profileHeaderView.viewModel.viewDidAppear.send()
// set display after view appear // set display after view appear
profileHeaderView.setupAvatarOverlayViews() profileHeaderView.setupImageOverlayViews()
} }
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews() {
@ -185,11 +188,15 @@ extension ProfileHeaderViewController {
} }
extension ProfileHeaderViewController { extension ProfileHeaderViewController {
private func createAvatarContextMenu() -> UIMenu { fileprivate enum ImageType {
case avatar
case banner
}
private func createImageContextMenu(_ type: ImageType) -> UIMenu {
var children: [UIMenuElement] = [] var children: [UIMenuElement] = []
let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .photoLibaray", ((#file as NSString).lastPathComponent), #line, #function) self.currentImageType = type
self.present(self.imagePicker, animated: true, completion: nil) self.present(self.imagePicker, animated: true, completion: nil)
} }
children.append(photoLibraryAction) children.append(photoLibraryAction)
@ -197,6 +204,7 @@ extension ProfileHeaderViewController {
let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .camera", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .camera", ((#file as NSString).lastPathComponent), #line, #function)
self.currentImageType = type
self.present(self.imagePickerController, animated: true, completion: nil) self.present(self.imagePickerController, animated: true, completion: nil)
}) })
children.append(cameraAction) children.append(cameraAction)
@ -204,6 +212,7 @@ extension ProfileHeaderViewController {
let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .browse", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .browse", ((#file as NSString).lastPathComponent), #line, #function)
self.currentImageType = type
self.present(self.documentPickerController, animated: true, completion: nil) self.present(self.documentPickerController, animated: true, completion: nil)
} }
children.append(browseAction) children.append(browseAction)
@ -443,7 +452,12 @@ extension ProfileHeaderViewController: UIDocumentPickerDelegate {
// MARK: - CropViewControllerDelegate // MARK: - CropViewControllerDelegate
extension ProfileHeaderViewController: CropViewControllerDelegate { extension ProfileHeaderViewController: CropViewControllerDelegate {
public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) { public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
switch currentImageType {
case .banner:
viewModel.profileInfoEditing.header = image
case .avatar:
viewModel.profileInfoEditing.avatar = image viewModel.profileInfoEditing.avatar = image
}
cropViewController.dismiss(animated: true, completion: nil) cropViewController.dismiss(animated: true, completion: nil)
} }
} }

View File

@ -52,6 +52,9 @@ final class ProfileHeaderViewModel {
.sink { [weak self] account in .sink { [weak self] account in
guard let self = self else { return } guard let self = self else { return }
guard let account = account else { return } guard let account = account else { return }
// banner
self.profileInfo.header = nil
self.profileInfoEditing.header = nil
// avatar // avatar
self.profileInfo.avatar = nil self.profileInfo.avatar = nil
self.profileInfoEditing.avatar = nil self.profileInfoEditing.avatar = nil
@ -72,6 +75,7 @@ final class ProfileHeaderViewModel {
extension ProfileHeaderViewModel { extension ProfileHeaderViewModel {
class ProfileInfo { class ProfileInfo {
// input // input
@Published var header: UIImage?
@Published var avatar: UIImage? @Published var avatar: UIImage?
@Published var name: String? @Published var name: String?
@Published var note: String? @Published var note: String?
@ -99,6 +103,7 @@ extension ProfileHeaderViewModel: ProfileViewModelEditable {
var isEdited: Bool { var isEdited: Bool {
guard isEditing else { return false } guard isEditing else { return false }
guard profileInfoEditing.header == nil else { return true }
guard profileInfoEditing.avatar == nil else { return true } guard profileInfoEditing.avatar == nil else { return true }
guard profileInfo.name == profileInfoEditing.name else { return true } guard profileInfo.name == profileInfoEditing.name else { return true }
guard profileInfo.note == profileInfoEditing.note else { return true } guard profileInfo.note == profileInfoEditing.note else { return true }

View File

@ -262,22 +262,29 @@ extension ProfileHeaderView {
animator.addAnimations { animator.addAnimations {
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor
self.nameTextFieldBackgroundView.backgroundColor = .clear self.nameTextFieldBackgroundView.backgroundColor = .clear
self.editBannerButton.alpha = 0
self.editAvatarBackgroundView.alpha = 0 self.editAvatarBackgroundView.alpha = 0
} }
animator.addCompletion { _ in animator.addCompletion { _ in
self.editBannerButton.isHidden = true
self.editAvatarBackgroundView.isHidden = true self.editAvatarBackgroundView.isHidden = true
self.bannerImageViewSingleTapGestureRecognizer.isEnabled = true
} }
case .editing: case .editing:
nameMetaText.textView.alpha = 0 nameMetaText.textView.alpha = 0
nameTextField.isEnabled = true nameTextField.isEnabled = true
nameTextField.alpha = 1 nameTextField.alpha = 1
editBannerButton.isHidden = false
editBannerButton.alpha = 0
editAvatarBackgroundView.isHidden = false editAvatarBackgroundView.isHidden = false
editAvatarBackgroundView.alpha = 0 editAvatarBackgroundView.alpha = 0
bioMetaText.textView.backgroundColor = .clear bioMetaText.textView.backgroundColor = .clear
bannerImageViewSingleTapGestureRecognizer.isEnabled = false
animator.addAnimations { animator.addAnimations {
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor
self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color
self.editBannerButton.alpha = 1
self.editAvatarBackgroundView.alpha = 1 self.editAvatarBackgroundView.alpha = 1
self.bioMetaText.textView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color self.bioMetaText.textView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
} }

View File

@ -50,6 +50,7 @@ final class ProfileHeaderView: UIView {
return viewModel return viewModel
}() }()
let bannerImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
let bannerContainerView = UIView() let bannerContainerView = UIView()
let bannerImageView: UIImageView = { let bannerImageView: UIImageView = {
let imageView = UIImageView() let imageView = UIImageView()
@ -101,7 +102,9 @@ final class ProfileHeaderView: UIView {
return button return button
}() }()
func setupAvatarOverlayViews() { func setupImageOverlayViews() {
editBannerButton.tintColor = .white
editAvatarBackgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.6) editAvatarBackgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.6)
editAvatarButtonOverlayIndicatorView.tintColor = .white editAvatarButtonOverlayIndicatorView.tintColor = .white
} }
@ -113,6 +116,13 @@ final class ProfileHeaderView: UIView {
return visualEffectView return visualEffectView
}() }()
let editBannerButton: HighlightDimmableButton = {
let button = HighlightDimmableButton()
button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 28)), for: .normal)
button.tintColor = .clear
return button
}()
let editAvatarBackgroundView: UIView = { let editAvatarBackgroundView: UIView = {
let view = UIView() let view = UIView()
view.backgroundColor = .clear // set value after view appeared view.backgroundColor = .clear // set value after view appeared
@ -272,6 +282,16 @@ extension ProfileHeaderView {
bannerImageViewOverlayVisualEffectView.bottomAnchor.constraint(equalTo: bannerImageView.bottomAnchor), bannerImageViewOverlayVisualEffectView.bottomAnchor.constraint(equalTo: bannerImageView.bottomAnchor),
]) ])
editBannerButton.translatesAutoresizingMaskIntoConstraints = false
bannerContainerView.addSubview(editBannerButton)
NSLayoutConstraint.activate([
editBannerButton.topAnchor.constraint(equalTo: bannerImageView.topAnchor),
editBannerButton.leadingAnchor.constraint(equalTo: bannerImageView.leadingAnchor),
editBannerButton.trailingAnchor.constraint(equalTo: bannerImageView.trailingAnchor),
editBannerButton.bottomAnchor.constraint(equalTo: bannerImageView.bottomAnchor),
])
bannerContainerView.isUserInteractionEnabled = true
// follows you // follows you
followsYouBlurEffectView.translatesAutoresizingMaskIntoConstraints = false followsYouBlurEffectView.translatesAutoresizingMaskIntoConstraints = false
addSubview(followsYouBlurEffectView) addSubview(followsYouBlurEffectView)
@ -456,7 +476,6 @@ extension ProfileHeaderView {
bioMetaText.textView.delegate = self bioMetaText.textView.delegate = self
bioMetaText.textView.linkDelegate = self bioMetaText.textView.linkDelegate = self
let bannerImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
bannerImageView.addGestureRecognizer(bannerImageViewSingleTapGestureRecognizer) bannerImageView.addGestureRecognizer(bannerImageViewSingleTapGestureRecognizer)
bannerImageViewSingleTapGestureRecognizer.addTarget(self, action: #selector(ProfileHeaderView.bannerImageViewDidPressed(_:))) bannerImageViewSingleTapGestureRecognizer.addTarget(self, action: #selector(ProfileHeaderView.bannerImageViewDidPressed(_:)))

View File

@ -216,7 +216,10 @@ extension ProfileViewModel {
let domain = authenticationBox.domain let domain = authenticationBox.domain
let authorization = authenticationBox.userAuthorization let authorization = authenticationBox.userAuthorization
let _image: UIImage? = { // TODO: constrain size?
let _header: UIImage? = headerProfileInfo.header
let _avatar: UIImage? = {
guard let image = headerProfileInfo.avatar else { return nil } guard let image = headerProfileInfo.avatar else { return nil }
guard image.size.width <= ProfileHeaderViewModel.avatarImageMaxSizeInPixel.width else { guard image.size.width <= ProfileHeaderViewModel.avatarImageMaxSizeInPixel.width else {
return image.af.imageScaled(to: ProfileHeaderViewModel.avatarImageMaxSizeInPixel) return image.af.imageScaled(to: ProfileHeaderViewModel.avatarImageMaxSizeInPixel)
@ -233,8 +236,8 @@ extension ProfileViewModel {
bot: nil, bot: nil,
displayName: headerProfileInfo.name, displayName: headerProfileInfo.name,
note: headerProfileInfo.note, note: headerProfileInfo.note,
avatar: _image.flatMap { Mastodon.Query.MediaAttachment.png($0.pngData()) }, avatar: _avatar.flatMap { Mastodon.Query.MediaAttachment.png($0.pngData()) },
header: nil, header: _header.flatMap { Mastodon.Query.MediaAttachment.png($0.pngData()) },
locked: nil, locked: nil,
source: nil, source: nil,
fieldsAttributes: fieldsAttributes fieldsAttributes: fieldsAttributes