2021-04-01 08:39:15 +02:00
|
|
|
//
|
|
|
|
// ProfileHeaderViewController.swift
|
|
|
|
// Mastodon
|
|
|
|
//
|
|
|
|
// Created by MainasuK Cirno on 2021-3-29.
|
|
|
|
//
|
|
|
|
|
|
|
|
import os.log
|
|
|
|
import UIKit
|
2021-04-08 10:53:32 +02:00
|
|
|
import Combine
|
2021-04-09 11:31:43 +02:00
|
|
|
import PhotosUI
|
|
|
|
import AlamofireImage
|
|
|
|
import CropViewController
|
|
|
|
import TwitterTextEditor
|
2021-06-29 13:27:40 +02:00
|
|
|
import MastodonMeta
|
2021-07-23 13:10:27 +02:00
|
|
|
import MetaTextKit
|
2021-04-01 08:39:15 +02:00
|
|
|
|
2021-05-08 05:03:34 +02:00
|
|
|
protocol ProfileHeaderViewControllerDelegate: AnyObject {
|
2021-04-01 08:39:15 +02:00
|
|
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
|
|
|
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, pageSegmentedControlValueChanged segmentedControl: UISegmentedControl, selectedSegmentIndex index: Int)
|
2021-07-23 13:10:27 +02:00
|
|
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta)
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
final class ProfileHeaderViewController: UIViewController {
|
|
|
|
|
|
|
|
static let segmentedControlHeight: CGFloat = 32
|
|
|
|
static let segmentedControlMarginHeight: CGFloat = 20
|
|
|
|
static let headerMinHeight: CGFloat = segmentedControlHeight + 2 * segmentedControlMarginHeight
|
|
|
|
|
2021-04-09 11:31:43 +02:00
|
|
|
var disposeBag = Set<AnyCancellable>()
|
2021-04-01 08:39:15 +02:00
|
|
|
weak var delegate: ProfileHeaderViewControllerDelegate?
|
|
|
|
|
2021-04-09 11:31:43 +02:00
|
|
|
var viewModel: ProfileHeaderViewModel!
|
2021-04-08 10:53:32 +02:00
|
|
|
|
2021-04-09 13:44:48 +02:00
|
|
|
let titleView: DoubleTitleLabelNavigationBarTitleView = {
|
|
|
|
let titleView = DoubleTitleLabelNavigationBarTitleView()
|
|
|
|
titleView.titleLabel.textColor = .white
|
2021-07-23 13:10:27 +02:00
|
|
|
titleView.titleLabel.textAttributes[.foregroundColor] = UIColor.white
|
2021-04-09 13:44:48 +02:00
|
|
|
titleView.titleLabel.alpha = 0
|
|
|
|
titleView.subtitleLabel.textColor = .white
|
|
|
|
titleView.subtitleLabel.alpha = 0
|
|
|
|
titleView.layer.masksToBounds = true
|
|
|
|
return titleView
|
|
|
|
}()
|
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
let profileHeaderView = ProfileHeaderView()
|
2021-04-01 08:39:15 +02:00
|
|
|
let pageSegmentedControl: UISegmentedControl = {
|
|
|
|
let segmenetedControl = UISegmentedControl(items: ["A", "B"])
|
|
|
|
segmenetedControl.selectedSegmentIndex = 0
|
|
|
|
return segmenetedControl
|
|
|
|
}()
|
|
|
|
|
|
|
|
private var isBannerPinned = false
|
|
|
|
private var bottomShadowAlpha: CGFloat = 0.0
|
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
// private var isAdjustBannerImageViewForSafeAreaInset = false
|
2021-04-01 08:39:15 +02:00
|
|
|
private var containerSafeAreaInset: UIEdgeInsets = .zero
|
2021-04-08 10:53:32 +02:00
|
|
|
|
2021-04-09 11:31:43 +02:00
|
|
|
private(set) lazy var imagePicker: PHPickerViewController = {
|
|
|
|
var configuration = PHPickerConfiguration()
|
|
|
|
configuration.filter = .images
|
|
|
|
configuration.selectionLimit = 1
|
|
|
|
|
|
|
|
let imagePicker = PHPickerViewController(configuration: configuration)
|
|
|
|
imagePicker.delegate = self
|
|
|
|
return imagePicker
|
|
|
|
}()
|
|
|
|
private(set) lazy var imagePickerController: UIImagePickerController = {
|
|
|
|
let imagePickerController = UIImagePickerController()
|
|
|
|
imagePickerController.sourceType = .camera
|
|
|
|
imagePickerController.delegate = self
|
|
|
|
return imagePickerController
|
|
|
|
}()
|
|
|
|
|
|
|
|
private(set) lazy var documentPickerController: UIDocumentPickerViewController = {
|
|
|
|
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image])
|
|
|
|
documentPickerController.delegate = self
|
|
|
|
return documentPickerController
|
|
|
|
}()
|
2021-04-01 08:39:15 +02:00
|
|
|
|
|
|
|
deinit {
|
|
|
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension ProfileHeaderViewController {
|
|
|
|
|
|
|
|
override func viewDidLoad() {
|
|
|
|
super.viewDidLoad()
|
2021-07-05 10:07:17 +02:00
|
|
|
|
2021-07-06 12:00:39 +02:00
|
|
|
view.backgroundColor = ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor
|
2021-07-05 10:07:17 +02:00
|
|
|
ThemeService.shared.currentTheme
|
|
|
|
.receive(on: RunLoop.main)
|
|
|
|
.sink { [weak self] theme in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.view.backgroundColor = theme.systemGroupedBackgroundColor
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-04-01 08:39:15 +02:00
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
profileHeaderView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
view.addSubview(profileHeaderView)
|
2021-04-01 08:39:15 +02:00
|
|
|
NSLayoutConstraint.activate([
|
2021-04-02 12:13:45 +02:00
|
|
|
profileHeaderView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
|
profileHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
|
profileHeaderView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
2021-04-01 08:39:15 +02:00
|
|
|
])
|
2021-04-02 12:13:45 +02:00
|
|
|
profileHeaderView.preservesSuperviewLayoutMargins = true
|
2021-04-01 08:39:15 +02:00
|
|
|
|
2021-05-27 07:56:55 +02:00
|
|
|
profileHeaderView.fieldCollectionView.delegate = self
|
|
|
|
viewModel.setupProfileFieldCollectionViewDiffableDataSource(
|
|
|
|
collectionView: profileHeaderView.fieldCollectionView,
|
|
|
|
profileFieldCollectionViewCellDelegate: self,
|
|
|
|
profileFieldAddEntryCollectionViewCellDelegate: self
|
|
|
|
)
|
2021-07-05 10:07:17 +02:00
|
|
|
|
2021-05-27 07:56:55 +02:00
|
|
|
let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ProfileHeaderViewController.longPressReorderGestureHandler(_:)))
|
|
|
|
profileHeaderView.fieldCollectionView.addGestureRecognizer(longPressReorderGesture)
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
pageSegmentedControl.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
view.addSubview(pageSegmentedControl)
|
|
|
|
NSLayoutConstraint.activate([
|
2021-04-02 12:13:45 +02:00
|
|
|
pageSegmentedControl.topAnchor.constraint(equalTo: profileHeaderView.bottomAnchor, constant: ProfileHeaderViewController.segmentedControlMarginHeight),
|
2021-04-01 08:39:15 +02:00
|
|
|
pageSegmentedControl.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
|
|
|
|
pageSegmentedControl.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
|
|
|
|
view.bottomAnchor.constraint(equalTo: pageSegmentedControl.bottomAnchor, constant: ProfileHeaderViewController.segmentedControlMarginHeight),
|
|
|
|
pageSegmentedControl.heightAnchor.constraint(equalToConstant: ProfileHeaderViewController.segmentedControlHeight).priority(.defaultHigh),
|
|
|
|
])
|
|
|
|
|
|
|
|
pageSegmentedControl.addTarget(self, action: #selector(ProfileHeaderViewController.pageSegmentedControlValueChanged(_:)), for: .valueChanged)
|
2021-04-09 13:44:48 +02:00
|
|
|
|
|
|
|
Publishers.CombineLatest(
|
|
|
|
viewModel.viewDidAppear.eraseToAnyPublisher(),
|
|
|
|
viewModel.isTitleViewContentOffsetSet.eraseToAnyPublisher()
|
|
|
|
)
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] viewDidAppear, isTitleViewContentOffsetDidSetted in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.titleView.titleLabel.alpha = viewDidAppear && isTitleViewContentOffsetDidSetted ? 1 : 0
|
|
|
|
self.titleView.subtitleLabel.alpha = viewDidAppear && isTitleViewContentOffsetDidSetted ? 1 : 0
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-04-08 10:53:32 +02:00
|
|
|
|
2021-04-09 11:31:43 +02:00
|
|
|
viewModel.needsSetupBottomShadow
|
2021-04-08 10:53:32 +02:00
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] needsSetupBottomShadow in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.setupBottomShadow()
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-04-09 11:31:43 +02:00
|
|
|
|
|
|
|
Publishers.CombineLatest4(
|
|
|
|
viewModel.isEditing.eraseToAnyPublisher(),
|
|
|
|
viewModel.displayProfileInfo.avatarImageResource.eraseToAnyPublisher(),
|
|
|
|
viewModel.editProfileInfo.avatarImageResource.eraseToAnyPublisher(),
|
|
|
|
viewModel.viewDidAppear.eraseToAnyPublisher()
|
|
|
|
)
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] isEditing, resource, editingResource, _ in
|
|
|
|
guard let self = self else { return }
|
|
|
|
let url: URL? = {
|
|
|
|
guard case let .url(url) = resource else { return nil }
|
|
|
|
return url
|
|
|
|
|
|
|
|
}()
|
|
|
|
let image: UIImage? = {
|
|
|
|
guard case let .image(image) = editingResource else { return nil }
|
|
|
|
return image
|
|
|
|
}()
|
|
|
|
self.profileHeaderView.configure(
|
|
|
|
with: AvatarConfigurableViewConfiguration(
|
|
|
|
avatarImageURL: image == nil ? url : nil, // set only when image empty
|
|
|
|
placeholderImage: image,
|
2021-04-28 14:10:17 +02:00
|
|
|
keepImageCorner: true // fit preview transitioning
|
2021-04-09 11:31:43 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-06-29 13:27:40 +02:00
|
|
|
Publishers.CombineLatest4(
|
|
|
|
viewModel.isEditing,
|
|
|
|
viewModel.displayProfileInfo.name.removeDuplicates(),
|
|
|
|
viewModel.editProfileInfo.name.removeDuplicates(),
|
2021-07-23 13:10:27 +02:00
|
|
|
viewModel.emojiMeta
|
2021-04-09 11:31:43 +02:00
|
|
|
)
|
|
|
|
.receive(on: DispatchQueue.main)
|
2021-07-23 13:10:27 +02:00
|
|
|
.sink { [weak self] isEditing, name, editingName, emojiMeta in
|
2021-04-09 11:31:43 +02:00
|
|
|
guard let self = self else { return }
|
2021-06-29 13:27:40 +02:00
|
|
|
do {
|
2021-07-23 13:10:27 +02:00
|
|
|
let mastodonContent = MastodonContent(content: name ?? " ", emojis: emojiMeta)
|
|
|
|
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
2021-06-29 13:27:40 +02:00
|
|
|
self.profileHeaderView.nameMetaText.configure(content: metaContent)
|
|
|
|
} catch {
|
|
|
|
assertionFailure()
|
|
|
|
}
|
2021-04-09 11:31:43 +02:00
|
|
|
self.profileHeaderView.nameTextField.text = isEditing ? editingName : name
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
|
2021-07-23 13:10:27 +02:00
|
|
|
Publishers.CombineLatest4(
|
|
|
|
viewModel.isEditing.removeDuplicates(),
|
|
|
|
viewModel.displayProfileInfo.note.removeDuplicates(),
|
|
|
|
viewModel.editProfileInfo.note.removeDuplicates(),
|
|
|
|
viewModel.emojiMeta.removeDuplicates()
|
2021-04-09 11:31:43 +02:00
|
|
|
)
|
|
|
|
.receive(on: DispatchQueue.main)
|
2021-07-23 13:10:27 +02:00
|
|
|
.sink { [weak self] isEditing, note, editingNote, emojiMeta in
|
2021-04-09 11:31:43 +02:00
|
|
|
guard let self = self else { return }
|
2021-07-07 09:39:57 +02:00
|
|
|
|
2021-07-23 13:10:27 +02:00
|
|
|
self.profileHeaderView.bioMetaText.textView.isEditable = isEditing
|
|
|
|
|
|
|
|
if isEditing {
|
|
|
|
if self.profileHeaderView.bioMetaText.backedString != note {
|
|
|
|
let metaContent = PlaintextMetaContent(string: editingNote ?? "")
|
|
|
|
self.profileHeaderView.bioMetaText.configure(content: metaContent)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let mastodonContent = MastodonContent(content: note ?? "", emojis: emojiMeta)
|
|
|
|
do {
|
|
|
|
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
|
|
|
self.profileHeaderView.bioMetaText.configure(content: metaContent)
|
|
|
|
} catch {
|
|
|
|
assertionFailure()
|
|
|
|
self.profileHeaderView.bioMetaText.reset()
|
|
|
|
}
|
2021-07-07 09:39:57 +02:00
|
|
|
}
|
2021-04-09 11:31:43 +02:00
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-07-23 13:10:27 +02:00
|
|
|
profileHeaderView.bioMetaText.delegate = self
|
|
|
|
|
2021-04-09 11:31:43 +02:00
|
|
|
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: profileHeaderView.nameTextField)
|
|
|
|
.receive(on: DispatchQueue.main)
|
|
|
|
.sink { [weak self] notification in
|
|
|
|
guard let self = self else { return }
|
|
|
|
guard let textField = notification.object as? UITextField else { return }
|
|
|
|
self.viewModel.editProfileInfo.name.value = textField.text
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
|
2021-07-06 11:53:01 +02:00
|
|
|
Publishers.CombineLatest3(
|
2021-05-27 07:56:55 +02:00
|
|
|
viewModel.isEditing,
|
2021-07-06 11:53:01 +02:00
|
|
|
viewModel.displayProfileInfo.fields,
|
|
|
|
viewModel.needsFiledCollectionViewHidden
|
2021-05-27 07:56:55 +02:00
|
|
|
)
|
|
|
|
.receive(on: RunLoop.main)
|
2021-07-06 11:53:01 +02:00
|
|
|
.sink { [weak self] isEditing, fields, needsHidden in
|
2021-05-27 07:56:55 +02:00
|
|
|
guard let self = self else { return }
|
2021-07-06 11:53:01 +02:00
|
|
|
guard !needsHidden else {
|
|
|
|
self.profileHeaderView.fieldCollectionView.isHidden = true
|
|
|
|
return
|
|
|
|
}
|
2021-05-27 07:56:55 +02:00
|
|
|
self.profileHeaderView.fieldCollectionView.isHidden = isEditing ? false : fields.isEmpty
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
|
2021-04-09 11:31:43 +02:00
|
|
|
profileHeaderView.editAvatarButton.menu = createAvatarContextMenu()
|
|
|
|
profileHeaderView.editAvatarButton.showsMenuAsPrimaryAction = true
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
|
|
super.viewDidAppear(animated)
|
|
|
|
|
2021-04-09 13:44:48 +02:00
|
|
|
viewModel.viewDidAppear.value = true
|
2021-06-22 13:33:36 +02:00
|
|
|
|
|
|
|
// set display after view appear
|
|
|
|
profileHeaderView.setupAvatarOverlayViews()
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
|
|
super.viewDidLayoutSubviews()
|
|
|
|
|
|
|
|
delegate?.profileHeaderViewController(self, viewLayoutDidUpdate: view)
|
2021-04-08 10:53:32 +02:00
|
|
|
setupBottomShadow()
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-04-09 11:31:43 +02:00
|
|
|
extension ProfileHeaderViewController {
|
|
|
|
private func createAvatarContextMenu() -> UIMenu {
|
|
|
|
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
|
|
|
|
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.present(self.imagePicker, animated: true, completion: nil)
|
|
|
|
}
|
|
|
|
children.append(photoLibraryAction)
|
|
|
|
if UIImagePickerController.isSourceTypeAvailable(.camera) {
|
|
|
|
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 }
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .camera", ((#file as NSString).lastPathComponent), #line, #function)
|
|
|
|
self.present(self.imagePickerController, animated: true, completion: nil)
|
|
|
|
})
|
|
|
|
children.append(cameraAction)
|
|
|
|
}
|
|
|
|
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 }
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .browse", ((#file as NSString).lastPathComponent), #line, #function)
|
|
|
|
self.present(self.documentPickerController, animated: true, completion: nil)
|
|
|
|
}
|
|
|
|
children.append(browseAction)
|
|
|
|
|
|
|
|
return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func cropImage(image: UIImage, pickerViewController: UIViewController) {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
let cropController = CropViewController(croppingStyle: .default, image: image)
|
|
|
|
cropController.delegate = self
|
|
|
|
cropController.setAspectRatioPreset(.presetSquare, animated: true)
|
|
|
|
cropController.aspectRatioPickerButtonHidden = true
|
|
|
|
cropController.aspectRatioLockEnabled = true
|
|
|
|
pickerViewController.dismiss(animated: true, completion: {
|
|
|
|
self.present(cropController, animated: true, completion: nil)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
extension ProfileHeaderViewController {
|
|
|
|
|
|
|
|
@objc private func pageSegmentedControlValueChanged(_ sender: UISegmentedControl) {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: selectedSegmentIndex: %ld", ((#file as NSString).lastPathComponent), #line, #function, sender.selectedSegmentIndex)
|
|
|
|
delegate?.profileHeaderViewController(self, pageSegmentedControlValueChanged: sender, selectedSegmentIndex: sender.selectedSegmentIndex)
|
|
|
|
}
|
|
|
|
|
2021-05-27 07:56:55 +02:00
|
|
|
// seealso: ProfileHeaderViewModel.setupProfileFieldCollectionViewDiffableDataSource(…)
|
|
|
|
@objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) {
|
|
|
|
guard sender.view === profileHeaderView.fieldCollectionView else {
|
|
|
|
assertionFailure()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let collectionView = profileHeaderView.fieldCollectionView
|
|
|
|
switch(sender.state) {
|
|
|
|
case .began:
|
|
|
|
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)),
|
|
|
|
let cell = collectionView.cellForItem(at: selectedIndexPath) as? ProfileFieldCollectionViewCell else {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
// check if pressing reorder bar no not
|
|
|
|
let locationInCell = sender.location(in: cell.reorderBarImageView)
|
|
|
|
guard cell.reorderBarImageView.bounds.contains(locationInCell) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
|
|
|
|
case .changed:
|
|
|
|
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)),
|
|
|
|
let diffableDataSource = viewModel.fieldDiffableDataSource else {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
guard let item = diffableDataSource.itemIdentifier(for: selectedIndexPath),
|
|
|
|
case .field = item else {
|
|
|
|
collectionView.cancelInteractiveMovement()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var position = sender.location(in: collectionView)
|
|
|
|
position.x = collectionView.frame.width * 0.5
|
|
|
|
collectionView.updateInteractiveMovementTargetPosition(position)
|
|
|
|
case .ended:
|
|
|
|
collectionView.endInteractiveMovement()
|
|
|
|
collectionView.reloadData()
|
|
|
|
default:
|
|
|
|
collectionView.cancelInteractiveMovement()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
extension ProfileHeaderViewController {
|
|
|
|
|
|
|
|
func updateHeaderContainerSafeAreaInset(_ inset: UIEdgeInsets) {
|
|
|
|
containerSafeAreaInset = inset
|
|
|
|
}
|
|
|
|
|
2021-04-08 10:53:32 +02:00
|
|
|
func setupBottomShadow() {
|
2021-04-09 11:31:43 +02:00
|
|
|
guard viewModel.needsSetupBottomShadow.value else {
|
2021-04-08 10:53:32 +02:00
|
|
|
view.layer.shadowColor = nil
|
|
|
|
view.layer.shadowRadius = 0
|
|
|
|
return
|
|
|
|
}
|
|
|
|
view.layer.setupShadow(color: UIColor.black.withAlphaComponent(0.12), alpha: Float(bottomShadowAlpha), x: 0, y: 2, blur: 2, spread: 0, roundedRect: view.bounds, byRoundingCorners: .allCorners, cornerRadii: .zero)
|
|
|
|
}
|
|
|
|
|
2021-04-01 08:39:15 +02:00
|
|
|
private func updateHeaderBottomShadow(progress: CGFloat) {
|
|
|
|
let alpha = min(max(0, 10 * progress - 9), 1)
|
|
|
|
if bottomShadowAlpha != alpha {
|
|
|
|
bottomShadowAlpha = alpha
|
|
|
|
view.setNeedsLayout()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-27 09:27:12 +02:00
|
|
|
func updateHeaderScrollProgress(_ progress: CGFloat, throttle: CGFloat) {
|
2021-04-01 08:39:15 +02:00
|
|
|
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
|
|
|
|
updateHeaderBottomShadow(progress: progress)
|
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
let bannerImageView = profileHeaderView.bannerImageView
|
2021-04-01 08:39:15 +02:00
|
|
|
guard bannerImageView.bounds != .zero else {
|
|
|
|
// wait layout finish
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-04-02 12:13:45 +02:00
|
|
|
let bannerContainerInWindow = profileHeaderView.convert(profileHeaderView.bannerContainerView.frame, to: nil)
|
2021-04-01 08:39:15 +02:00
|
|
|
let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height
|
2021-04-09 13:44:48 +02:00
|
|
|
|
|
|
|
// scroll from bottom to top: 1 -> 2 -> 3
|
2021-04-01 08:39:15 +02:00
|
|
|
if bannerContainerInWindow.origin.y > containerSafeAreaInset.top {
|
2021-04-09 13:44:48 +02:00
|
|
|
// 1
|
|
|
|
// banner top pin to window top and expand
|
2021-04-01 08:39:15 +02:00
|
|
|
bannerImageView.frame.origin.y = -bannerContainerInWindow.origin.y
|
|
|
|
bannerImageView.frame.size.height = bannerContainerInWindow.origin.y + bannerContainerInWindow.size.height
|
|
|
|
} else if bannerContainerBottomOffset < containerSafeAreaInset.top {
|
2021-04-09 13:44:48 +02:00
|
|
|
// 3
|
|
|
|
// banner bottom pin to navigation bar bottom and
|
2021-06-24 10:50:20 +02:00
|
|
|
// the `progress` growth to 1 then segmented control pin to top
|
2021-04-01 08:39:15 +02:00
|
|
|
bannerImageView.frame.origin.y = -containerSafeAreaInset.top
|
|
|
|
let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset)
|
|
|
|
bannerImageView.frame.size.height = bannerImageHeight
|
|
|
|
} else {
|
2021-04-09 13:44:48 +02:00
|
|
|
// 2
|
|
|
|
// banner move with scrolling from bottom to top until the
|
|
|
|
// banner bottom higher than navigation bar bottom
|
2021-04-01 08:39:15 +02:00
|
|
|
bannerImageView.frame.origin.y = -containerSafeAreaInset.top
|
|
|
|
bannerImageView.frame.size.height = bannerContainerInWindow.size.height + containerSafeAreaInset.top
|
|
|
|
}
|
|
|
|
|
2021-04-09 13:44:48 +02:00
|
|
|
// set title view offset
|
|
|
|
let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil)
|
|
|
|
let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y
|
|
|
|
let titleViewContentOffset: CGFloat = titleView.frame.height - nameTextFieldTopToNavigationBarBottomOffset
|
2021-04-29 11:13:13 +02:00
|
|
|
let transformY = max(0, titleViewContentOffset)
|
|
|
|
titleView.containerView.transform = CGAffineTransform(translationX: 0, y: transformY)
|
|
|
|
viewModel.isTitleViewDisplaying.value = transformY < titleView.containerView.frame.height
|
2021-04-09 13:44:48 +02:00
|
|
|
|
|
|
|
if viewModel.viewDidAppear.value {
|
|
|
|
viewModel.isTitleViewContentOffsetSet.value = true
|
|
|
|
}
|
|
|
|
|
2021-05-27 09:27:12 +02:00
|
|
|
// set avatar fade
|
2021-04-09 13:44:48 +02:00
|
|
|
if progress > 0 {
|
|
|
|
setProfileBannerFade(alpha: 0)
|
2021-05-27 09:27:12 +02:00
|
|
|
} else if progress > -abs(throttle) {
|
|
|
|
// y = -(1/0.8T)x
|
|
|
|
let alpha = -1 / abs(0.8 * throttle) * progress
|
2021-04-09 13:44:48 +02:00
|
|
|
setProfileBannerFade(alpha: alpha)
|
|
|
|
} else {
|
|
|
|
setProfileBannerFade(alpha: 1)
|
|
|
|
}
|
|
|
|
}
|
2021-06-24 10:50:20 +02:00
|
|
|
|
2021-04-09 13:44:48 +02:00
|
|
|
private func setProfileBannerFade(alpha: CGFloat) {
|
2021-04-29 11:13:13 +02:00
|
|
|
profileHeaderView.avatarImageViewBackgroundView.alpha = alpha
|
2021-04-09 13:44:48 +02:00
|
|
|
profileHeaderView.avatarImageView.alpha = alpha
|
|
|
|
profileHeaderView.editAvatarBackgroundView.alpha = alpha
|
|
|
|
profileHeaderView.nameTextFieldBackgroundView.alpha = alpha
|
2021-06-29 13:27:40 +02:00
|
|
|
profileHeaderView.displayNameStackView.alpha = alpha
|
2021-04-09 13:44:48 +02:00
|
|
|
profileHeaderView.usernameLabel.alpha = alpha
|
2021-04-01 08:39:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2021-04-09 11:31:43 +02:00
|
|
|
|
2021-07-23 13:10:27 +02:00
|
|
|
// MARK: - MetaTextDelegate
|
|
|
|
extension ProfileHeaderViewController: MetaTextDelegate {
|
|
|
|
func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, metaText.backedString)
|
|
|
|
assert(metaText.textView === profileHeaderView.bioMetaText.textView)
|
|
|
|
if metaText.textView === profileHeaderView.bioMetaText.textView {
|
|
|
|
viewModel.editProfileInfo.note.value = metaText.backedString
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2021-04-09 11:31:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - PHPickerViewControllerDelegate
|
|
|
|
extension ProfileHeaderViewController: PHPickerViewControllerDelegate {
|
|
|
|
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
|
|
|
picker.dismiss(animated: true, completion: nil)
|
|
|
|
guard let result = results.first else { return }
|
2021-07-19 11:12:45 +02:00
|
|
|
ItemProviderLoader.loadImageData(from: result)
|
2021-04-09 11:31:43 +02:00
|
|
|
.sink { [weak self] completion in
|
|
|
|
guard let _ = self else { return }
|
|
|
|
switch completion {
|
|
|
|
case .failure:
|
|
|
|
// TODO: handle error
|
|
|
|
break
|
|
|
|
case .finished:
|
|
|
|
break
|
|
|
|
}
|
2021-05-31 10:42:49 +02:00
|
|
|
} receiveValue: { [weak self] file in
|
2021-04-09 11:31:43 +02:00
|
|
|
guard let self = self else { return }
|
2021-05-31 10:42:49 +02:00
|
|
|
guard let imageData = file?.data else { return }
|
2021-04-09 11:31:43 +02:00
|
|
|
guard let image = UIImage(data: imageData) else { return }
|
|
|
|
self.cropImage(image: image, pickerViewController: picker)
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - UIImagePickerControllerDelegate
|
|
|
|
extension ProfileHeaderViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate {
|
|
|
|
|
|
|
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
|
|
|
picker.dismiss(animated: true, completion: nil)
|
|
|
|
|
|
|
|
guard let image = info[.originalImage] as? UIImage else { return }
|
|
|
|
cropImage(image: image, pickerViewController: picker)
|
|
|
|
}
|
|
|
|
|
|
|
|
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
|
|
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
|
|
picker.dismiss(animated: true, completion: nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - UIDocumentPickerDelegate
|
|
|
|
extension ProfileHeaderViewController: UIDocumentPickerDelegate {
|
|
|
|
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
|
|
|
guard let url = urls.first else { return }
|
|
|
|
|
|
|
|
do {
|
|
|
|
guard url.startAccessingSecurityScopedResource() else { return }
|
|
|
|
defer { url.stopAccessingSecurityScopedResource() }
|
|
|
|
let imageData = try Data(contentsOf: url)
|
|
|
|
guard let image = UIImage(data: imageData) else { return }
|
|
|
|
cropImage(image: image, pickerViewController: controller)
|
|
|
|
} catch {
|
|
|
|
os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - CropViewControllerDelegate
|
|
|
|
extension ProfileHeaderViewController: CropViewControllerDelegate {
|
|
|
|
public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
|
|
|
|
viewModel.editProfileInfo.avatarImageResource.value = .image(image)
|
|
|
|
cropViewController.dismiss(animated: true, completion: nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-27 07:56:55 +02:00
|
|
|
// MARK: - UICollectionViewDelegate
|
|
|
|
extension ProfileHeaderViewController: UICollectionViewDelegate {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - ProfileFieldCollectionViewCellDelegate
|
|
|
|
extension ProfileHeaderViewController: ProfileFieldCollectionViewCellDelegate {
|
2021-07-23 13:10:27 +02:00
|
|
|
|
2021-06-24 13:20:41 +02:00
|
|
|
// should be remove style edit button
|
2021-05-27 07:56:55 +02:00
|
|
|
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, editButtonDidPressed button: UIButton) {
|
|
|
|
guard let diffableDataSource = viewModel.fieldDiffableDataSource else { return }
|
|
|
|
guard let indexPath = profileHeaderView.fieldCollectionView.indexPath(for: cell) else { return }
|
|
|
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
|
|
|
viewModel.removeFieldItem(item: item)
|
|
|
|
}
|
|
|
|
|
2021-07-23 13:10:27 +02:00
|
|
|
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, metaLebel: MetaLabel, didSelectMeta meta: Meta) {
|
|
|
|
delegate?.profileHeaderViewController(self, profileFieldCollectionViewCell: cell, metaLabel: metaLebel, didSelectMeta: meta)
|
2021-05-27 07:56:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - ProfileFieldAddEntryCollectionViewCellDelegate
|
|
|
|
extension ProfileHeaderViewController: ProfileFieldAddEntryCollectionViewCellDelegate {
|
|
|
|
func ProfileFieldAddEntryCollectionViewCellDidPressed(_ cell: ProfileFieldAddEntryCollectionViewCell) {
|
|
|
|
viewModel.appendFieldItem()
|
|
|
|
}
|
|
|
|
}
|