// Copyright © 2020 Metabolist. All rights reserved. import Combine import Kingfisher import UIKit import ViewModels final class CompositionView: UIView { let avatarImageView = UIImageView() let spoilerTextField = UITextField() let textView = UITextView() let textViewPlaceholder = UILabel() let attachmentsView = AttachmentsView() let attachmentUploadView: AttachmentUploadView let markAttachmentsSensitiveView: MarkAttachmentsSensitiveView private let viewModel: CompositionViewModel private let parentViewModel: NewStatusViewModel private var cancellables = Set() init(viewModel: CompositionViewModel, parentViewModel: NewStatusViewModel) { self.viewModel = viewModel self.parentViewModel = parentViewModel attachmentUploadView = AttachmentUploadView(viewModel: viewModel) markAttachmentsSensitiveView = MarkAttachmentsSensitiveView(viewModel: viewModel) super.init(frame: .zero) initialSetup() } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension CompositionView { var id: CompositionViewModel.Id { viewModel.id } } extension CompositionView: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { viewModel.text = textView.text } } private extension CompositionView { static let attachmentCollectionViewHeight: CGFloat = 200 // swiftlint:disable:next function_body_length func initialSetup() { tag = viewModel.id.hashValue addSubview(avatarImageView) avatarImageView.translatesAutoresizingMaskIntoConstraints = false avatarImageView.layer.cornerRadius = .avatarDimension / 2 avatarImageView.clipsToBounds = true let stackView = UIStackView() let inputAccessoryView = CompositionInputAccessoryView(viewModel: viewModel, parentViewModel: parentViewModel) addSubview(stackView) stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.spacing = .defaultSpacing stackView.addArrangedSubview(spoilerTextField) spoilerTextField.borderStyle = .roundedRect spoilerTextField.adjustsFontForContentSizeCategory = true spoilerTextField.font = .preferredFont(forTextStyle: .body) spoilerTextField.placeholder = NSLocalizedString("status.spoiler-text-placeholder", comment: "") spoilerTextField.inputAccessoryView = inputAccessoryView spoilerTextField.addAction( UIAction { [weak self] _ in guard let self = self, let text = self.spoilerTextField.text else { return } self.viewModel.contentWarning = text }, for: .editingChanged) let textViewFont = UIFont.preferredFont(forTextStyle: .body) stackView.addArrangedSubview(textView) textView.isScrollEnabled = false textView.adjustsFontForContentSizeCategory = true textView.font = textViewFont textView.textContainerInset = .zero textView.textContainer.lineFragmentPadding = 0 textView.inputAccessoryView = inputAccessoryView textView.inputAccessoryView?.sizeToFit() textView.delegate = self textView.addSubview(textViewPlaceholder) textViewPlaceholder.translatesAutoresizingMaskIntoConstraints = false textViewPlaceholder.adjustsFontForContentSizeCategory = true textViewPlaceholder.font = .preferredFont(forTextStyle: .body) textViewPlaceholder.textColor = .secondaryLabel textViewPlaceholder.text = NSLocalizedString("compose.prompt", comment: "") stackView.addArrangedSubview(attachmentsView) stackView.addArrangedSubview(attachmentUploadView) stackView.addArrangedSubview(markAttachmentsSensitiveView) textView.text = viewModel.text spoilerTextField.text = viewModel.contentWarning let textViewBaselineConstraint = textView.topAnchor.constraint( lessThanOrEqualTo: avatarImageView.centerYAnchor, constant: -textViewFont.lineHeight / 2) viewModel.$text.map(\.isEmpty) .sink { [weak self] in self?.textViewPlaceholder.isHidden = !$0 } .store(in: &cancellables) viewModel.$displayContentWarning .sink { [weak self] in guard let self = self else { return } if self.spoilerTextField.isHidden && self.textView.isFirstResponder && $0 { self.spoilerTextField.becomeFirstResponder() } else if !self.spoilerTextField.isHidden && self.spoilerTextField.isFirstResponder && !$0 { self.textView.becomeFirstResponder() } self.spoilerTextField.isHidden = !$0 textViewBaselineConstraint.isActive = !$0 } .store(in: &cancellables) parentViewModel.$identification.map(\.identity.image) .sink { [weak self] in self?.avatarImageView.kf.setImage(with: $0) } .store(in: &cancellables) viewModel.$attachmentViewModels .receive(on: RunLoop.main) .sink { [weak self] in self?.attachmentsView.viewModel = self?.viewModel self?.attachmentsView.isHidden = $0.isEmpty self?.markAttachmentsSensitiveView.isHidden = $0.isEmpty } .store(in: &cancellables) let guide = UIDevice.current.userInterfaceIdiom == .pad ? readableContentGuide : layoutMarginsGuide let constraints = [ avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension), avatarImageView.widthAnchor.constraint(equalToConstant: .avatarDimension), avatarImageView.topAnchor.constraint(equalTo: guide.topAnchor), avatarImageView.leadingAnchor.constraint(equalTo: guide.leadingAnchor), avatarImageView.bottomAnchor.constraint(lessThanOrEqualTo: guide.bottomAnchor), stackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: .defaultSpacing), stackView.topAnchor.constraint(greaterThanOrEqualTo: guide.topAnchor), stackView.trailingAnchor.constraint(equalTo: guide.trailingAnchor), stackView.bottomAnchor.constraint(lessThanOrEqualTo: guide.bottomAnchor), textViewPlaceholder.leadingAnchor.constraint(equalTo: textView.leadingAnchor), textViewPlaceholder.topAnchor.constraint(equalTo: textView.topAnchor), textViewPlaceholder.trailingAnchor.constraint(equalTo: textView.trailingAnchor) ] if UIDevice.current.userInterfaceIdiom == .pad { for constraint in constraints { constraint.priority = .justBelowMax } } NSLayoutConstraint.activate(constraints) } }