mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Made a tweak to prevent some odd looking keyboard transitions when going to conversation settings Updated the PagedDatabaseObserver to not call 'onChangeUnsorted' on the main thread (now we can generate the changeset on the background thread so there is less main thread work) Fixed an issue where the most recently received message from the swarm could be removed from the swarm yet the app would still poll for it, resulting in the swarm always returning the oldest possible messages until the user sends a new one-to-one message Fixed an issue where the initial scroll offset could be incorrect due to certain message types Fixed an issue where the title view inside a conversation could jump when pushing to the conversation settings screen Refactored a couple of ObjC functions to Swift as they were crashing (due to memory allocation?) hopefully this will fix it Tweaked some DispatchQueue priorities to ensure PagedDatabaseObserver loading is prioritised Updated buttons to use a standard convention for highlighted states Updated the new conversation button to follow the new highlighted state convention
586 lines
22 KiB
Swift
586 lines
22 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import QuartzCore
|
|
import GRDB
|
|
import DifferenceKit
|
|
import SessionUIKit
|
|
import SignalUtilitiesKit
|
|
|
|
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 {
|
|
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)
|
|
|
|
if #available(iOS 15.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: MediaStrings.document,
|
|
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) {
|
|
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.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.hasLoadedInitialData = true
|
|
self.viewModel.updateGalleryData(updatedGalleryData)
|
|
|
|
UIView.performWithoutAnimation {
|
|
self.tableView.reloadData()
|
|
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 "DOCUMENT_TILES_EMPTY_DOCUMENT".localized()
|
|
case .loadOlder: return "DOCUMENT_TILES_LOADING_OLDER_LABEL".localized()
|
|
case .loadNewer: return "DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL".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 static let iconImageViewSize: CGSize = CGSize(width: 31, height: 40)
|
|
|
|
private let iconImageView: UIImageView = {
|
|
let result: UIImageView = UIImageView(image: #imageLiteral(resourceName: "File").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)
|
|
}
|
|
|
|
// 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)
|
|
),
|
|
|
|
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)
|
|
),
|
|
])
|
|
}
|
|
|
|
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.sourceFilename ?? "File")
|
|
detailLabel.text = "\(OWSFormat.formatFileSize(UInt(attachment.byteCount)))"
|
|
timeLabel.text = Date(
|
|
timeIntervalSince1970: TimeInterval(item.interactionTimestampMs / 1000)
|
|
).formattedForDisplay
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|