mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
608 lines
23 KiB
Swift
608 lines
23 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import QuartzCore
|
|
import GRDB
|
|
import DifferenceKit
|
|
import SessionUIKit
|
|
import SignalUtilitiesKit
|
|
import SignalCoreKit
|
|
import SessionUtilitiesKit
|
|
|
|
public class DocumentTileViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
|
|
|
|
/// This should be larger than one screen size so we don't have to call it multiple times in rapid succession, but not
|
|
/// so large that loading get's really chopping
|
|
static let itemPageSize: Int = Int(11 * itemsPerPortraitRow)
|
|
static let itemsPerPortraitRow: CGFloat = 4
|
|
static let interItemSpacing: CGFloat = 2
|
|
static let footerBarHeight: CGFloat = 40
|
|
static let loadMoreHeaderHeight: CGFloat = 100
|
|
|
|
private let viewModel: MediaGalleryViewModel
|
|
private var hasLoadedInitialData: Bool = false
|
|
private var didFinishInitialLayout: Bool = false
|
|
private var isAutoLoadingNextPage: Bool = false
|
|
private var currentTargetOffset: CGPoint?
|
|
|
|
public weak var delegate: DocumentTileViewControllerDelegate?
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(viewModel: MediaGalleryViewModel) {
|
|
self.viewModel = viewModel
|
|
Storage.shared.addObserver(viewModel.pagedDataObserver)
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
notImplemented()
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
// MARK: - UI
|
|
|
|
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
|
if UIDevice.current.isIPad {
|
|
return .all
|
|
}
|
|
|
|
return .allButUpsideDown
|
|
}
|
|
|
|
lazy var tableView: UITableView = {
|
|
let result: UITableView = UITableView()
|
|
result.themeBackgroundColor = .newConversation_background
|
|
result.separatorStyle = .none
|
|
result.showsVerticalScrollIndicator = false
|
|
result.register(view: DocumentCell.self)
|
|
result.delegate = self
|
|
result.dataSource = self
|
|
// Feels a bit weird to have content smashed all the way to the bottom edge.
|
|
result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0)
|
|
result.sectionHeaderTopPadding = 0
|
|
|
|
return result
|
|
}()
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
override public func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
// Add a custom back button if this is the only view controller
|
|
if self.navigationController?.viewControllers.first == self {
|
|
let backButton = UIViewController.createOWSBackButton(target: self, selector: #selector(didPressDismissButton))
|
|
self.navigationItem.leftBarButtonItem = backButton
|
|
}
|
|
|
|
ViewControllerUtilities.setUpDefaultSessionStyle(
|
|
for: self,
|
|
title:"files".localized(),
|
|
hasCustomBackButton: false
|
|
)
|
|
|
|
view.addSubview(self.tableView)
|
|
tableView.autoPin(toEdgesOf: view)
|
|
|
|
// Notifications
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(applicationDidBecomeActive(_:)),
|
|
name: UIApplication.didBecomeActiveNotification,
|
|
object: nil
|
|
)
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(applicationDidResignActive(_:)),
|
|
name: UIApplication.didEnterBackgroundNotification, object: nil
|
|
)
|
|
}
|
|
|
|
public override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
startObservingChanges()
|
|
}
|
|
|
|
public override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
self.didFinishInitialLayout = true
|
|
}
|
|
|
|
public override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
stopObservingChanges()
|
|
}
|
|
|
|
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
|
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.startObservingChanges()
|
|
}
|
|
}
|
|
|
|
@objc func applicationDidResignActive(_ notification: Notification) {
|
|
stopObservingChanges()
|
|
}
|
|
|
|
// MARK: - Updating
|
|
|
|
private func performInitialScrollIfNeeded() {
|
|
// Ensure this hasn't run before and that we have data (The 'galleryData' will always
|
|
// contain something as the 'empty' state is a section within 'galleryData')
|
|
guard !self.didFinishInitialLayout && self.hasLoadedInitialData else { return }
|
|
|
|
// If we have a focused item then we want to scroll to it
|
|
guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return }
|
|
|
|
Logger.debug("scrolling to focused item at indexPath: \(focusedIndexPath)")
|
|
self.view.layoutIfNeeded()
|
|
self.tableView.scrollToRow(at: focusedIndexPath, at: .middle, animated: false)
|
|
|
|
// Now that the data has loaded we need to check if either of the "load more" sections are
|
|
// visible and trigger them if so
|
|
//
|
|
// Note: We do it this way as we want to trigger the load behaviour for the first section
|
|
// if it has one before trying to trigger the load behaviour for the last section
|
|
self.autoLoadNextPageIfNeeded()
|
|
}
|
|
|
|
private func autoLoadNextPageIfNeeded() {
|
|
guard self.hasLoadedInitialData && !self.isAutoLoadingNextPage else { return }
|
|
|
|
self.isAutoLoadingNextPage = true
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + PagedData.autoLoadNextPageDelay) { [weak self] in
|
|
self?.isAutoLoadingNextPage = false
|
|
|
|
// Note: We sort the headers as we want to prioritise loading newer pages over older ones
|
|
let sortedVisibleIndexPaths: [IndexPath] = (self?.tableView.indexPathsForVisibleRows ?? []).sorted()
|
|
|
|
for headerIndexPath in sortedVisibleIndexPaths {
|
|
let section: MediaGalleryViewModel.SectionModel? = self?.viewModel.galleryData[safe: headerIndexPath.section]
|
|
|
|
switch section?.model {
|
|
case .loadNewer, .loadOlder:
|
|
// Attachments are loaded in descending order so 'loadOlder' actually corresponds with
|
|
// 'pageAfter' in this case
|
|
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
self?.viewModel.pagedDataObserver?.load(section?.model == .loadOlder ?
|
|
.pageAfter :
|
|
.pageBefore
|
|
)
|
|
}
|
|
return
|
|
|
|
default: continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func startObservingChanges() {
|
|
// Start observing for data changes (will callback on the main thread)
|
|
self.viewModel.onGalleryChange = { [weak self] updatedGalleryData, changeset in
|
|
self?.handleUpdates(updatedGalleryData, changeset: changeset)
|
|
}
|
|
}
|
|
|
|
private func stopObservingChanges() {
|
|
// Note: The 'pagedDataObserver' will continue to get changes but
|
|
// we don't want to trigger any UI updates
|
|
self.viewModel.onGalleryChange = nil
|
|
}
|
|
|
|
private func handleUpdates(
|
|
_ updatedGalleryData: [MediaGalleryViewModel.SectionModel],
|
|
changeset: StagedChangeset<[MediaGalleryViewModel.SectionModel]>
|
|
) {
|
|
// Ensure the first load runs without animations (if we don't do this the cells will animate
|
|
// in from a frame of CGRect.zero)
|
|
guard hasLoadedInitialData else {
|
|
self.viewModel.updateGalleryData(updatedGalleryData)
|
|
|
|
UIView.performWithoutAnimation {
|
|
self.tableView.reloadData()
|
|
self.hasLoadedInitialData = true
|
|
self.performInitialScrollIfNeeded()
|
|
}
|
|
return
|
|
}
|
|
|
|
let isInsertingAtTop: Bool = {
|
|
let oldFirstSectionIsLoadMore: Bool = (
|
|
self.viewModel.galleryData.first?.model == .loadNewer ||
|
|
self.viewModel.galleryData.first?.model == .loadOlder
|
|
)
|
|
let oldTargetSectionIndex: Int = (oldFirstSectionIsLoadMore ? 1 : 0)
|
|
|
|
guard
|
|
let newTargetSectionIndex = updatedGalleryData
|
|
.firstIndex(where: { $0.model == self.viewModel.galleryData[safe: oldTargetSectionIndex]?.model }),
|
|
let oldFirstItem: MediaGalleryViewModel.Item = self.viewModel.galleryData[safe: oldTargetSectionIndex]?.elements.first,
|
|
let newFirstItemIndex = updatedGalleryData[safe: newTargetSectionIndex]?.elements.firstIndex(of: oldFirstItem)
|
|
else { return false }
|
|
|
|
return (newTargetSectionIndex > oldTargetSectionIndex || newFirstItemIndex > 0)
|
|
}()
|
|
|
|
CATransaction.begin()
|
|
|
|
if isInsertingAtTop { CATransaction.setDisableActions(true) }
|
|
|
|
self.tableView.reload(
|
|
using: changeset,
|
|
with: .automatic,
|
|
interrupt: { $0.changeCount > MediaTileViewController.itemPageSize }
|
|
) { [weak self] updatedData in
|
|
self?.viewModel.updateGalleryData(updatedData)
|
|
}
|
|
|
|
CATransaction.setCompletionBlock { [weak self] in
|
|
// If one of the "load more" sections is still visible once the animation completes then
|
|
// trigger another "load more" (after a small delay to minimize animation bugginess)
|
|
self?.autoLoadNextPageIfNeeded()
|
|
}
|
|
CATransaction.commit()
|
|
|
|
}
|
|
|
|
// MARK: - Interactions
|
|
|
|
@objc public func didPressDismissButton() {
|
|
let presentedNavController: UINavigationController? = (self.presentingViewController as? UINavigationController)
|
|
let mediaPageViewController: MediaPageViewController? = (
|
|
(presentedNavController?.viewControllers.last as? MediaPageViewController) ??
|
|
(self.presentingViewController as? MediaPageViewController)
|
|
)
|
|
|
|
// If the album was presented from a 'MediaPageViewController' and it has no more data (ie.
|
|
// all album items had been deleted) then dismiss to the screen before that one
|
|
guard mediaPageViewController?.viewModel.albumData.isEmpty != true else {
|
|
presentedNavController?.presentingViewController?.dismiss(animated: true, completion: nil)
|
|
return
|
|
}
|
|
|
|
dismiss(animated: true, completion: nil)
|
|
}
|
|
|
|
// MARK: - UITableViewDataSource
|
|
|
|
public func numberOfSections(in tableView: UITableView) -> Int {
|
|
return self.viewModel.galleryData.count
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
return self.viewModel.galleryData[section].elements.count
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
let cell: DocumentCell = tableView.dequeue(type: DocumentCell.self, for: indexPath)
|
|
cell.update(with: self.viewModel.galleryData[indexPath.section].elements[indexPath.row])
|
|
return cell
|
|
}
|
|
|
|
// MARK: - UITableViewDelegate
|
|
|
|
public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
|
let section: MediaGalleryViewModel.SectionModel = self.viewModel.galleryData[section]
|
|
|
|
switch section.model {
|
|
case .emptyGallery, .loadOlder, .loadNewer:
|
|
let headerView: DocumentStaticHeaderView = DocumentStaticHeaderView()
|
|
headerView.configure(
|
|
title: {
|
|
switch section.model {
|
|
case .emptyGallery: return "attachmentsFilesEmpty".localized()
|
|
case .loadOlder: return "attachmentsLoadingOlderFiles".localized()
|
|
case .loadNewer: return "attachmentsLoadingNewerFiles".localized()
|
|
case .galleryMonth: return "" // Impossible case
|
|
}
|
|
}()
|
|
)
|
|
return headerView
|
|
|
|
case .galleryMonth(let date):
|
|
let headerView: DocumentSectionHeaderView = DocumentSectionHeaderView()
|
|
headerView.configure(title: date.localizedString)
|
|
return headerView
|
|
}
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
|
let section: MediaGalleryViewModel.SectionModel = self.viewModel.galleryData[section]
|
|
|
|
switch section.model {
|
|
case .emptyGallery, .loadOlder, .loadNewer:
|
|
return MediaTileViewController.loadMoreHeaderHeight
|
|
|
|
case .galleryMonth:
|
|
return 50
|
|
}
|
|
}
|
|
|
|
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
tableView.deselectRow(at: indexPath, animated: false)
|
|
let attachment: Attachment = self.viewModel.galleryData[indexPath.section].elements[indexPath.row].attachment
|
|
guard let originalFilePath: String = attachment.originalFilePath else { return }
|
|
|
|
let fileUrl: URL = URL(fileURLWithPath: originalFilePath)
|
|
|
|
// Open a preview of the document for text, pdf or microsoft files
|
|
if
|
|
attachment.isText ||
|
|
attachment.isMicrosoftDoc ||
|
|
attachment.contentType == OWSMimeTypeApplicationPdf
|
|
{
|
|
|
|
delegate?.preview(fileUrl: fileUrl)
|
|
return
|
|
}
|
|
|
|
// Otherwise share the file
|
|
delegate?.share(fileUrl: fileUrl)
|
|
}
|
|
}
|
|
|
|
// MARK: - View
|
|
|
|
class DocumentCell: UITableViewCell {
|
|
|
|
// MARK: - Initialization
|
|
|
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
|
|
setUpViewHierarchy()
|
|
setupLayout()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
|
|
setUpViewHierarchy()
|
|
setupLayout()
|
|
}
|
|
|
|
// MARK: - UI
|
|
|
|
private let iconImageView: UIImageView = {
|
|
let result: UIImageView = UIImageView(image: UIImage(systemName: "doc")?.withRenderingMode(.alwaysTemplate))
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.themeTintColor = .textPrimary
|
|
result.contentMode = .scaleAspectFit
|
|
|
|
return result
|
|
}()
|
|
|
|
private let audioImageView: UIImageView = {
|
|
let result = UIImageView(image: UIImage(systemName: "music.note")?.withRenderingMode(.alwaysTemplate))
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.themeTintColor = .textPrimary
|
|
result.contentMode = .scaleAspectFit
|
|
|
|
return result
|
|
}()
|
|
|
|
private let titleLabel: UILabel = {
|
|
let result: UILabel = UILabel()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
result.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
result.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
|
result.themeTextColor = .textPrimary
|
|
result.lineBreakMode = .byTruncatingTail
|
|
result.numberOfLines = 2
|
|
|
|
return result
|
|
}()
|
|
|
|
private let timeLabel: UILabel = {
|
|
let result: UILabel = UILabel()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
result.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
result.font = .systemFont(ofSize: Values.smallFontSize)
|
|
result.themeTextColor = .textSecondary
|
|
result.lineBreakMode = .byTruncatingTail
|
|
|
|
return result
|
|
}()
|
|
|
|
private let detailLabel: UILabel = {
|
|
let result: UILabel = UILabel()
|
|
result.translatesAutoresizingMaskIntoConstraints = false
|
|
result.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
result.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
result.font = .systemFont(ofSize: Values.smallFontSize)
|
|
result.themeTextColor = .textSecondary
|
|
result.lineBreakMode = .byTruncatingTail
|
|
|
|
return result
|
|
}()
|
|
|
|
private func setUpViewHierarchy() {
|
|
themeBackgroundColor = .clear
|
|
|
|
backgroundView = UIView()
|
|
backgroundView?.themeBackgroundColor = .settings_tabBackground
|
|
backgroundView?.layer.cornerRadius = 5
|
|
|
|
selectedBackgroundView = UIView()
|
|
selectedBackgroundView?.themeBackgroundColor = .highlighted(.settings_tabBackground)
|
|
selectedBackgroundView?.layer.cornerRadius = 5
|
|
|
|
contentView.addSubview(iconImageView)
|
|
contentView.addSubview(titleLabel)
|
|
contentView.addSubview(timeLabel)
|
|
contentView.addSubview(detailLabel)
|
|
|
|
iconImageView.addSubview(audioImageView)
|
|
}
|
|
|
|
// MARK: - Layout
|
|
|
|
private func setupLayout() {
|
|
NSLayoutConstraint.activate([
|
|
iconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
|
iconImageView.topAnchor.constraint(
|
|
greaterThanOrEqualTo: contentView.topAnchor,
|
|
constant: (Values.verySmallSpacing + Values.verySmallSpacing)
|
|
),
|
|
iconImageView.leftAnchor.constraint(
|
|
equalTo: contentView.leftAnchor,
|
|
constant: (Values.largeSpacing + Values.mediumSpacing)
|
|
),
|
|
iconImageView.bottomAnchor.constraint(
|
|
lessThanOrEqualTo: contentView.bottomAnchor,
|
|
constant: -(Values.verySmallSpacing + Values.verySmallSpacing)
|
|
),
|
|
iconImageView.widthAnchor.constraint(equalToConstant: 36),
|
|
iconImageView.heightAnchor.constraint(equalToConstant: 46),
|
|
|
|
titleLabel.topAnchor.constraint(
|
|
equalTo: contentView.topAnchor,
|
|
constant: (Values.verySmallSpacing + Values.verySmallSpacing)
|
|
),
|
|
titleLabel.leftAnchor.constraint(equalTo: iconImageView.rightAnchor, constant: Values.mediumSpacing),
|
|
titleLabel.rightAnchor.constraint(
|
|
lessThanOrEqualTo: timeLabel.leftAnchor,
|
|
constant: -Values.mediumSpacing
|
|
),
|
|
|
|
timeLabel.topAnchor.constraint(equalTo: iconImageView.topAnchor),
|
|
timeLabel.rightAnchor.constraint(
|
|
equalTo: contentView.rightAnchor,
|
|
constant: -(Values.mediumSpacing + Values.largeSpacing)
|
|
),
|
|
|
|
detailLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: Values.smallSpacing),
|
|
detailLabel.leftAnchor.constraint(equalTo: iconImageView.rightAnchor, constant: Values.mediumSpacing),
|
|
detailLabel.rightAnchor.constraint(
|
|
lessThanOrEqualTo: contentView.rightAnchor,
|
|
constant: -(Values.verySmallSpacing + Values.largeSpacing)
|
|
),
|
|
detailLabel.bottomAnchor.constraint(
|
|
lessThanOrEqualTo: contentView.bottomAnchor,
|
|
constant: -(Values.verySmallSpacing + Values.smallSpacing)
|
|
),
|
|
|
|
audioImageView.centerXAnchor.constraint(equalTo: iconImageView.centerXAnchor),
|
|
audioImageView.centerYAnchor.constraint(equalTo: iconImageView.centerYAnchor, constant: 7),
|
|
audioImageView.heightAnchor.constraint(equalTo: iconImageView.heightAnchor, multiplier: 0.32)
|
|
])
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
backgroundView?.frame = CGRect(
|
|
x: Values.largeSpacing,
|
|
y: Values.verySmallSpacing,
|
|
width: (contentView.bounds.width - (Values.largeSpacing * 2)),
|
|
height: (contentView.bounds.height - (Values.verySmallSpacing * 2))
|
|
)
|
|
selectedBackgroundView?.frame = (backgroundView?.frame ?? .zero)
|
|
}
|
|
|
|
// MARK: - Content
|
|
|
|
func update(with item: MediaGalleryViewModel.Item) {
|
|
let attachment = item.attachment
|
|
titleLabel.text = attachment.documentFileName
|
|
detailLabel.text = attachment.documentFileInfo
|
|
timeLabel.text = Date(
|
|
timeIntervalSince1970: TimeInterval(item.interactionTimestampMs / 1000)
|
|
).formattedForDisplay
|
|
audioImageView.isHidden = !attachment.isAudio
|
|
}
|
|
}
|
|
|
|
class DocumentSectionHeaderView: UIView {
|
|
// HACK: scrollbar incorrectly appears *behind* section headers
|
|
// in collection view on iOS11 =(
|
|
private class AlwaysOnTopLayer: CALayer {
|
|
override var zPosition: CGFloat {
|
|
get { return 0 }
|
|
set {}
|
|
}
|
|
}
|
|
|
|
let label: UILabel
|
|
|
|
override class var layerClass: AnyClass {
|
|
get {
|
|
// HACK: scrollbar incorrectly appears *behind* section headers
|
|
// in collection view on iOS11 =(
|
|
return AlwaysOnTopLayer.self
|
|
}
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
label = UILabel()
|
|
label.themeTextColor = .textPrimary
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.themeBackgroundColor = .clear
|
|
|
|
let backgroundView: UIView = UIView()
|
|
backgroundView.themeBackgroundColor = .newConversation_background
|
|
addSubview(backgroundView)
|
|
backgroundView.pin(to: self)
|
|
|
|
self.addSubview(label)
|
|
label.pin(.leading, to: .leading, of: self, withInset: Values.largeSpacing)
|
|
label.pin(.trailing, to: .trailing, of: self, withInset: -Values.largeSpacing)
|
|
label.center(.vertical, in: self)
|
|
}
|
|
|
|
@available(*, unavailable, message: "Unimplemented")
|
|
required init?(coder aDecoder: NSCoder) {
|
|
notImplemented()
|
|
}
|
|
|
|
public func configure(title: String) {
|
|
self.label.text = title
|
|
}
|
|
}
|
|
|
|
class DocumentStaticHeaderView: UIView {
|
|
|
|
let label = UILabel()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
addSubview(label)
|
|
|
|
label.themeTextColor = .textPrimary
|
|
label.textAlignment = .center
|
|
label.numberOfLines = 0
|
|
label.autoPinEdgesToSuperviewMargins(with: UIEdgeInsets(top: 0, leading: Values.largeSpacing, bottom: 0, trailing: Values.largeSpacing))
|
|
}
|
|
|
|
@available(*, unavailable, message: "Unimplemented")
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
notImplemented()
|
|
}
|
|
|
|
public func configure(title: String) {
|
|
self.label.text = title
|
|
}
|
|
}
|
|
|
|
// MARK: - DocumentTitleViewControllerDelegate
|
|
|
|
public protocol DocumentTileViewControllerDelegate: AnyObject {
|
|
func share(fileUrl: URL)
|
|
func preview(fileUrl: URL)
|
|
}
|