This commit is contained in:
Justin Mazzocchi 2021-01-02 17:22:17 -08:00
parent 5ae1d9be9a
commit 4806c43202
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
11 changed files with 122 additions and 44 deletions

View file

@ -9,10 +9,11 @@ final class CompositionAttachmentsDataSource: UICollectionViewDiffableDataSource
DispatchQueue(label: "com.metabolist.metatext.composition-attachments-data-source.update-queue") DispatchQueue(label: "com.metabolist.metatext.composition-attachments-data-source.update-queue")
init(collectionView: UICollectionView, init(collectionView: UICollectionView,
viewModelProvider: @escaping (IndexPath) -> CompositionAttachmentViewModel?) { viewModelProvider: @escaping (IndexPath) -> (CompositionAttachmentViewModel, CompositionViewModel)) {
let registration = UICollectionView.CellRegistration let registration = UICollectionView.CellRegistration
<CompositionAttachmentCollectionViewCell, CompositionAttachmentViewModel> { <CompositionAttachmentCollectionViewCell, (CompositionAttachmentViewModel, CompositionViewModel)> {
$0.viewModel = $2 $0.viewModel = $2.0
$0.parentViewModel = $2.1
} }
super.init(collectionView: collectionView) { collectionView, indexPath, _ in super.init(collectionView: collectionView) { collectionView, indexPath, _ in

View file

@ -38,6 +38,8 @@
"camera-access.description" = "Open system settings to allow camera access"; "camera-access.description" = "Open system settings to allow camera access";
"camera-access.open-system-settings" = "Open system settings"; "camera-access.open-system-settings" = "Open system settings";
"cancel" = "Cancel"; "cancel" = "Cancel";
"compose.attachment.uploading" = "Uploading";
"compose.attachment.remove" = "Remove";
"error" = "Error"; "error" = "Error";
"favorites" = "Favorites"; "favorites" = "Favorites";
"registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue"; "registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue";

View file

@ -44,4 +44,19 @@ public struct Attachment: Codable, Hashable {
public extension Attachment { public extension Attachment {
typealias Id = String typealias Id = String
var aspectRatio: Double? {
if
let info = meta?.original,
let width = info.width,
let height = info.height,
width != 0,
height != 0 {
let aspectRatio = Double(width) / Double(height)
return aspectRatio.isNaN ? nil : aspectRatio
}
return nil
}
} }

View file

@ -22,21 +22,6 @@ public extension AttachmentViewModel {
attachment.id.appending(status.id).hashValue attachment.id.appending(status.id).hashValue
} }
var aspectRatio: Double? {
if
let info = attachment.meta?.original,
let width = info.width,
let height = info.height,
width != 0,
height != 0 {
let aspectRatio = Double(width) / Double(height)
return aspectRatio.isNaN ? nil : aspectRatio
}
return nil
}
var shouldAutoplay: Bool { var shouldAutoplay: Bool {
switch attachment.type { switch attachment.type {
case .video: case .video:

View file

@ -46,8 +46,8 @@ public extension CompositionViewModel {
visibility: visibility) visibility: visibility)
} }
func attachmentViewModel(indexPath: IndexPath) -> CompositionAttachmentViewModel { func remove(attachmentViewModel: CompositionAttachmentViewModel) {
attachmentViewModels[indexPath.item] attachmentViewModels.removeAll { $0 === attachmentViewModel }
} }
} }

View file

@ -5,6 +5,7 @@ import UIKit
import ViewModels import ViewModels
final class AttachmentUploadView: UIView { final class AttachmentUploadView: UIView {
let label = UILabel()
let progressView = UIProgressView(progressViewStyle: .default) let progressView = UIProgressView(progressViewStyle: .default)
private var progressCancellable: AnyCancellable? private var progressCancellable: AnyCancellable?
@ -24,13 +25,26 @@ final class AttachmentUploadView: UIView {
init() { init() {
super.init(frame: .zero) super.init(frame: .zero)
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.adjustsFontForContentSizeCategory = true
label.font = .preferredFont(forTextStyle: .callout)
label.textAlignment = .center
label.text = NSLocalizedString("compose.attachment.uploading", comment: "")
label.textColor = .secondaryLabel
label.numberOfLines = 0
addSubview(progressView) addSubview(progressView)
progressView.translatesAutoresizingMaskIntoConstraints = false progressView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
label.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
progressView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: .defaultSpacing),
progressView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), progressView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
progressView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), progressView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
progressView.centerYAnchor.constraint(equalTo: centerYAnchor) progressView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
]) ])
} }

View file

@ -5,11 +5,15 @@ import ViewModels
class CompositionAttachmentCollectionViewCell: UICollectionViewCell { class CompositionAttachmentCollectionViewCell: UICollectionViewCell {
var viewModel: CompositionAttachmentViewModel? var viewModel: CompositionAttachmentViewModel?
var parentViewModel: CompositionViewModel?
override func updateConfiguration(using state: UICellConfigurationState) { override func updateConfiguration(using state: UICellConfigurationState) {
guard let viewModel = viewModel else { return } guard let viewModel = viewModel, let parentViewModel = parentViewModel else { return }
contentConfiguration = CompositionAttachmentContentConfiguration(viewModel: viewModel).updated(for: state) contentConfiguration = CompositionAttachmentContentConfiguration(
viewModel: viewModel,
parentViewModel: parentViewModel)
.updated(for: state)
backgroundConfiguration = UIBackgroundConfiguration.clear().updated(for: state) backgroundConfiguration = UIBackgroundConfiguration.clear().updated(for: state)
} }
} }

View file

@ -5,6 +5,7 @@ import ViewModels
struct CompositionAttachmentContentConfiguration { struct CompositionAttachmentContentConfiguration {
let viewModel: CompositionAttachmentViewModel let viewModel: CompositionAttachmentViewModel
let parentViewModel: CompositionViewModel
} }
extension CompositionAttachmentContentConfiguration: UIContentConfiguration { extension CompositionAttachmentContentConfiguration: UIContentConfiguration {

View file

@ -6,11 +6,16 @@ import ViewModels
class CompositionAttachmentView: UIView { class CompositionAttachmentView: UIView {
let imageView = UIImageView() let imageView = UIImageView()
let removeButton = UIButton()
let editButton = UIButton()
private var compositionAttachmentConfiguration: CompositionAttachmentContentConfiguration private var compositionAttachmentConfiguration: CompositionAttachmentContentConfiguration
private var aspectRatioConstraint: NSLayoutConstraint
init(configuration: CompositionAttachmentContentConfiguration) { init(configuration: CompositionAttachmentContentConfiguration) {
self.compositionAttachmentConfiguration = configuration self.compositionAttachmentConfiguration = configuration
aspectRatioConstraint = imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 2)
super.init(frame: .zero) super.init(frame: .zero)
initialSetup() initialSetup()
@ -38,22 +43,70 @@ extension CompositionAttachmentView: UIContentView {
} }
private extension CompositionAttachmentView { private extension CompositionAttachmentView {
// swiftlint:disable:next function_body_length
func initialSetup() { func initialSetup() {
backgroundColor = .secondarySystemBackground
layer.cornerRadius = .defaultCornerRadius
clipsToBounds = true
addSubview(imageView) addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = .defaultCornerRadius imageView.kf.indicatorType = .activity
imageView.clipsToBounds = true
addSubview(removeButton)
removeButton.translatesAutoresizingMaskIntoConstraints = false
removeButton.setImage(
UIImage(
systemName: "xmark.circle.fill",
withConfiguration: UIImage.SymbolConfiguration(scale: .large)),
for: .normal)
removeButton.showsMenuAsPrimaryAction = true
removeButton.menu = UIMenu(
children: [
UIAction(
title: NSLocalizedString("compose.attachment.remove", comment: ""),
image: UIImage(systemName: "xmark.circle.fill"),
attributes: .destructive, handler: { [weak self] _ in
guard let self = self else { return }
self.compositionAttachmentConfiguration.parentViewModel.remove(
attachmentViewModel: self.compositionAttachmentConfiguration.viewModel)
})])
addSubview(editButton)
editButton.translatesAutoresizingMaskIntoConstraints = false
editButton.setImage(
UIImage(
systemName: "pencil.circle.fill",
withConfiguration: UIImage.SymbolConfiguration(scale: .large)),
for: .normal)
editButton.addAction(UIAction { [weak self] _ in }, for: .touchUpInside)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
aspectRatioConstraint,
imageView.leadingAnchor.constraint(equalTo: leadingAnchor), imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
imageView.topAnchor.constraint(equalTo: topAnchor), imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor), imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor) imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
removeButton.topAnchor.constraint(equalTo: topAnchor),
removeButton.trailingAnchor.constraint(equalTo: trailingAnchor),
removeButton.heightAnchor.constraint(equalToConstant: .minimumButtonDimension),
removeButton.widthAnchor.constraint(equalToConstant: .minimumButtonDimension),
editButton.trailingAnchor.constraint(equalTo: trailingAnchor),
editButton.bottomAnchor.constraint(equalTo: bottomAnchor),
editButton.heightAnchor.constraint(equalToConstant: .minimumButtonDimension),
editButton.widthAnchor.constraint(equalToConstant: .minimumButtonDimension)
]) ])
} }
func applyCompositionAttachmentConfiguration() { func applyCompositionAttachmentConfiguration() {
imageView.kf.setImage(with: compositionAttachmentConfiguration.viewModel.attachment.previewUrl) imageView.kf.setImage(with: compositionAttachmentConfiguration.viewModel.attachment.previewUrl)
aspectRatioConstraint.isActive = false
aspectRatioConstraint = imageView.widthAnchor.constraint(
equalTo: imageView.heightAnchor,
multiplier: CGFloat(compositionAttachmentConfiguration.viewModel.attachment.aspectRatio ?? 1))
aspectRatioConstraint.priority = .justBelowMax
aspectRatioConstraint.isActive = true
} }
} }

View file

@ -17,9 +17,10 @@ final class CompositionView: UIView {
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private lazy var attachmentsDataSource: CompositionAttachmentsDataSource = { private lazy var attachmentsDataSource: CompositionAttachmentsDataSource = {
CompositionAttachmentsDataSource( let vm = viewModel
collectionView: attachmentsCollectionView) { [weak self] in
self?.viewModel.attachmentViewModel(indexPath: $0) return .init(collectionView: attachmentsCollectionView) {
(vm.attachmentViewModels[$0.item], vm)
} }
}() }()
@ -28,20 +29,25 @@ final class CompositionView: UIView {
self.parentViewModel = parentViewModel self.parentViewModel = parentViewModel
let itemSize = NSCollectionLayoutSize( let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.2), widthDimension: .estimated(Self.attachmentCollectionViewHeight),
heightDimension: .fractionalHeight(1.0)) heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize) let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize( let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0), widthDimension: .estimated(Self.attachmentCollectionViewHeight),
heightDimension: .fractionalWidth(0.2)) heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal( let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize, layoutSize: groupSize,
subitems: [item]) subitems: [item])
group.interItemSpacing = .fixed(.defaultSpacing)
let section = NSCollectionLayoutSection(group: group) let section = NSCollectionLayoutSection(group: group)
let attachmentsLayout = UICollectionViewCompositionalLayout(section: section)
section.interGroupSpacing = .defaultSpacing
let configuration = UICollectionViewCompositionalLayoutConfiguration()
configuration.scrollDirection = .horizontal
let attachmentsLayout = UICollectionViewCompositionalLayout(section: section, configuration: configuration)
attachmentsCollectionView = UICollectionView(frame: .zero, collectionViewLayout: attachmentsLayout) attachmentsCollectionView = UICollectionView(frame: .zero, collectionViewLayout: attachmentsLayout)
super.init(frame: .zero) super.init(frame: .zero)
@ -66,7 +72,7 @@ extension CompositionView: UITextViewDelegate {
} }
private extension CompositionView { private extension CompositionView {
static let attachmentUploadViewHeight: CGFloat = 100 static let attachmentCollectionViewHeight: CGFloat = 200
// swiftlint:disable:next function_body_length // swiftlint:disable:next function_body_length
func initialSetup() { func initialSetup() {
@ -114,6 +120,7 @@ private extension CompositionView {
stackView.addArrangedSubview(attachmentsCollectionView) stackView.addArrangedSubview(attachmentsCollectionView)
attachmentsCollectionView.dataSource = attachmentsDataSource attachmentsCollectionView.dataSource = attachmentsDataSource
attachmentsCollectionView.backgroundColor = .clear
stackView.addArrangedSubview(attachmentUploadView) stackView.addArrangedSubview(attachmentUploadView)
@ -139,7 +146,6 @@ private extension CompositionView {
.store(in: &cancellables) .store(in: &cancellables)
viewModel.$attachmentViewModels viewModel.$attachmentViewModels
.receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring
.sink { [weak self] in .sink { [weak self] in
self?.attachmentsDataSource.apply($0.map(\.attachment).snapshot()) self?.attachmentsDataSource.apply($0.map(\.attachment).snapshot())
self?.attachmentsCollectionView.isHidden = $0.isEmpty self?.attachmentsCollectionView.isHidden = $0.isEmpty
@ -161,10 +167,7 @@ private extension CompositionView {
stackView.topAnchor.constraint(equalTo: guide.topAnchor), stackView.topAnchor.constraint(equalTo: guide.topAnchor),
stackView.trailingAnchor.constraint(equalTo: guide.trailingAnchor), stackView.trailingAnchor.constraint(equalTo: guide.trailingAnchor),
stackView.bottomAnchor.constraint(lessThanOrEqualTo: guide.bottomAnchor), stackView.bottomAnchor.constraint(lessThanOrEqualTo: guide.bottomAnchor),
attachmentsCollectionView.heightAnchor.constraint( attachmentsCollectionView.heightAnchor.constraint(equalToConstant: Self.attachmentCollectionViewHeight)
equalTo: attachmentsCollectionView.widthAnchor,
multiplier: 1 / 4),
attachmentUploadView.heightAnchor.constraint(equalToConstant: Self.attachmentUploadViewHeight)
] ]
if UIDevice.current.userInterfaceIdiom == .pad { if UIDevice.current.userInterfaceIdiom == .pad {

View file

@ -47,7 +47,7 @@ final class StatusAttachmentsView: UIView {
let newAspectRatio: CGFloat let newAspectRatio: CGFloat
if attachmentCount == 1, let aspectRatio = attachmentViewModels.first?.aspectRatio { if attachmentCount == 1, let aspectRatio = attachmentViewModels.first?.attachment.aspectRatio {
newAspectRatio = max(CGFloat(aspectRatio), 16 / 9) newAspectRatio = max(CGFloat(aspectRatio), 16 / 9)
} else { } else {
newAspectRatio = 16 / 9 newAspectRatio = 16 / 9