Implement linkification

This commit is contained in:
Niels Andriesse 2021-02-11 14:24:38 +11:00
parent aa027a28c5
commit 401a29344d
8 changed files with 219 additions and 98 deletions

View File

@ -216,6 +216,7 @@
B8041A9525C8FA1D003C2166 /* MediaLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8041A9425C8FA1D003C2166 /* MediaLoaderView.swift */; };
B8041AA725C90927003C2166 /* TypingIndicatorCellV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8041AA625C90927003C2166 /* TypingIndicatorCellV2.swift */; };
B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */; };
B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821494525D4D6FF009C0F2A /* URLModal.swift */; };
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D2825C7A4B400488AB4 /* InputView.swift */; };
B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3225C7A8C600488AB4 /* InputViewButton.swift */; };
B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3C25C7B34D00488AB4 /* InputTextView.swift */; };
@ -279,7 +280,7 @@
B88A1AC725C90A4700E6D421 /* TypingIndicatorInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B6A904218B4C90007C4606 /* TypingIndicatorInteraction.swift */; };
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; };
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; };
B897621C25D201F7004F83B2 /* ConversationVC+ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ConversationVC+ScrollToBottomButton.swift */; };
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; };
B8A14D702589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A14D6F2589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift */; };
B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */; };
B8AE760B25ABFB5A001A84D2 /* GeneralUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = B8AE760A25ABFB5A001A84D2 /* GeneralUtilities.m */; };
@ -1261,6 +1262,7 @@
B8041A9425C8FA1D003C2166 /* MediaLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderView.swift; sourceTree = "<group>"; };
B8041AA625C90927003C2166 /* TypingIndicatorCellV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorCellV2.swift; sourceTree = "<group>"; };
B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewClosedGroupVC.swift; sourceTree = "<group>"; };
B821494525D4D6FF009C0F2A /* URLModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLModal.swift; sourceTree = "<group>"; };
B8269D2825C7A4B400488AB4 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = "<group>"; };
B8269D3225C7A8C600488AB4 /* InputViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewButton.swift; sourceTree = "<group>"; };
B8269D3C25C7B34D00488AB4 /* InputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = "<group>"; };
@ -1303,7 +1305,7 @@
B88847BB23E10BC6009836D2 /* GroupMembersVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMembersVC.swift; sourceTree = "<group>"; };
B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = "<group>"; };
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = "<group>"; };
B897621B25D201F7004F83B2 /* ConversationVC+ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationVC+ScrollToBottomButton.swift"; sourceTree = "<group>"; };
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
B8A14D6F2589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPairMigrationSuccessSheet.swift; sourceTree = "<group>"; };
B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Trimming.swift"; sourceTree = "<group>"; };
B8AE760925ABFB00001A84D2 /* GeneralUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneralUtilities.h; sourceTree = "<group>"; };
@ -2229,15 +2231,24 @@
path = "Content Views";
sourceTree = "<group>";
};
B821493625D4D6A7009C0F2A /* Views & Modals */ = {
isa = PBXGroup;
children = (
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */,
B821494525D4D6FF009C0F2A /* URLModal.swift */,
);
path = "Views & Modals";
sourceTree = "<group>";
};
B835246C25C38AA20089A44F /* Conversations V2 */ = {
isa = PBXGroup;
children = (
B835246D25C38ABF0089A44F /* ConversationVC.swift */,
B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */,
B897621B25D201F7004F83B2 /* ConversationVC+ScrollToBottomButton.swift */,
B887C38125C7C79700E11DAE /* Input View */,
B835247725C38D190089A44F /* Message Cells */,
C328252E25CA54F70062D0A7 /* Context Menu */,
B821493625D4D6A7009C0F2A /* Views & Modals */,
);
path = "Conversations V2";
sourceTree = "<group>";
@ -5027,6 +5038,7 @@
34D1F0881F8678AA0066283D /* ConversationViewLayout.m in Sources */,
B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */,
B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */,
B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */,
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */,
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */,
344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */,
@ -5115,7 +5127,7 @@
340FC8B0204DAC8D007AEB0F /* AddToBlockListViewController.m in Sources */,
3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */,
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */,
B897621C25D201F7004F83B2 /* ConversationVC+ScrollToBottomButton.swift in Sources */,
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */,
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */,
45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */,
34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */,

View File

@ -201,4 +201,11 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
func handleQuoteViewCancelButtonTapped() {
snInputView.quoteDraftInfo = nil
}
func openURL(_ url: URL) {
let urlModal = URLModal(url: url)
urlModal.modalPresentationStyle = .overFullScreen
urlModal.modalTransitionStyle = .crossDissolve
present(urlModal, animated: true, completion: nil)
}
}

View File

@ -1,70 +0,0 @@
extension ConversationVC {
final class ScrollToBottomButton : UIView {
private let delegate: ScrollToBottomButtonDelegate
// MARK: Settings
private static let size: CGFloat = 40
private static let iconSize: CGFloat = 16
// MARK: Lifecycle
init(delegate: ScrollToBottomButtonDelegate) {
self.delegate = delegate
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(delegate:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(delegate:) instead.")
}
private func setUpViewHierarchy() {
// Background & blur
let backgroundView = UIView()
backgroundView.backgroundColor = isLightMode ? .white : .black
backgroundView.alpha = Values.lowOpacity
addSubview(backgroundView)
backgroundView.pin(to: self)
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
addSubview(blurView)
blurView.pin(to: self)
// Size & shape
let size = ScrollToBottomButton.size
set(.width, to: size)
set(.height, to: size)
layer.cornerRadius = size / 2
layer.masksToBounds = true
// Border
layer.borderWidth = 1
let borderColor = (isLightMode ? UIColor.black : UIColor.white).withAlphaComponent(Values.veryLowOpacity)
layer.borderColor = borderColor.cgColor
// Icon
let tint = isLightMode ? UIColor.black : UIColor.white
let icon = UIImage(named: "ic_chevron_down")!.withTint(tint)
let iconImageView = UIImageView(image: icon)
iconImageView.set(.width, to: ScrollToBottomButton.iconSize)
iconImageView.set(.height, to: ScrollToBottomButton.iconSize)
iconImageView.contentMode = .scaleAspectFit
addSubview(iconImageView)
iconImageView.center(in: self)
// Gesture recognizer
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tapGestureRecognizer)
}
// MARK: Interaction
@objc private func handleTap() {
delegate.handleScrollToBottomButtonTapped()
}
}
}
protocol ScrollToBottomButtonDelegate {
func handleScrollToBottomButtonTapped()
}

View File

@ -1,6 +1,8 @@
final class LinkView : UIView {
private let viewItem: ConversationViewItem
private let maxWidth: CGFloat
private let delegate: UITextViewDelegate
private var textColor: UIColor {
let isOutgoing = (viewItem.interaction.interactionType() == .outgoingMessage)
@ -12,18 +14,20 @@ final class LinkView : UIView {
private static let imageSize: CGFloat = 100
init(for viewItem: ConversationViewItem) {
init(for viewItem: ConversationViewItem, maxWidth: CGFloat, delegate: UITextViewDelegate) {
self.viewItem = viewItem
self.maxWidth = maxWidth
self.delegate = delegate
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(for:) instead.")
preconditionFailure("Use init(for:maxWidth:delegate:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(for:) instead.")
preconditionFailure("Use init(for:maxWidth:delegate:) instead.")
}
private func setUpViewHierarchy() {
@ -72,12 +76,12 @@ final class LinkView : UIView {
separator.set(.height, to: 1 / UIScreen.main.scale)
vStackView.addArrangedSubview(separator)
let bodyLabelContainer = UIView()
let bodyTextViewContainer = UIView()
let bodyLabel = VisibleMessageCell.getBodyLabel(for: viewItem, with: textColor)
bodyLabelContainer.addSubview(bodyLabel)
bodyLabel.pin(to: bodyLabelContainer, withInset: 12)
vStackView.addArrangedSubview(bodyLabelContainer)
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: textColor, delegate: delegate)
bodyTextViewContainer.addSubview(bodyTextView)
bodyTextView.pin(to: bodyTextViewContainer, withInset: 12)
vStackView.addArrangedSubview(bodyTextViewContainer)
addSubview(vStackView)
vStackView.pin(to: self)

View File

@ -55,4 +55,5 @@ protocol MessageCellDelegate {
func handleViewItemTapped(_ viewItem: ConversationViewItem, gestureRecognizer: UITapGestureRecognizer)
func handleViewItemDoubleTapped(_ viewItem: ConversationViewItem)
func showFullText(_ viewItem: ConversationViewItem)
func openURL(_ url: URL)
}

View File

@ -1,7 +1,8 @@
final class VisibleMessageCell : MessageCell {
final class VisibleMessageCell : MessageCell, UITextViewDelegate {
private var unloadContent: (() -> Void)?
var albumView: MediaAlbumView?
var bodyTextView: UITextView?
var mediaTextOverlayView: MediaTextOverlayView?
// Constraints
private lazy var headerViewTopConstraint = headerView.pin(.top, to: .top, of: self, withInset: 1)
@ -103,7 +104,6 @@ final class VisibleMessageCell : MessageCell {
// MARK: Lifecycle
override func setUpViewHierarchy() {
super.setUpViewHierarchy()
isUserInteractionEnabled = true
// Header view
addSubview(headerView)
headerViewTopConstraint.isActive = true
@ -224,32 +224,34 @@ final class VisibleMessageCell : MessageCell {
private func populateContentView(for viewItem: ConversationViewItem) {
snContentView.subviews.forEach { $0.removeFromSuperview() }
albumView = nil
bodyTextView = nil
mediaTextOverlayView = nil
let isOutgoing = (viewItem.interaction.interactionType() == .outgoingMessage)
switch viewItem.messageCellType {
case .textOnlyMessage:
let inset: CGFloat = 12
let maxWidth = VisibleMessageCell.getMaxWidth(for: viewItem) - 2 * inset
if viewItem.linkPreview != nil {
let linkView = LinkView(for: viewItem)
let linkView = LinkView(for: viewItem, maxWidth: maxWidth, delegate: self)
snContentView.addSubview(linkView)
linkView.pin(to: snContentView)
} else {
let inset: CGFloat = 12
// Stack view
let stackView = UIStackView(arrangedSubviews: [])
stackView.axis = .vertical
stackView.spacing = 2
// Quote view
if viewItem.quotedReply != nil {
let maxWidth = VisibleMessageCell.getMaxWidth(for: viewItem) - 2 * inset
let direction: QuoteView.Direction = isOutgoing ? .outgoing : .incoming
let hInset: CGFloat = 2
let quoteView = QuoteView(for: viewItem, direction: direction, hInset: hInset, maxWidth: maxWidth)
let quoteViewContainer = UIView(wrapping: quoteView, withInsets: UIEdgeInsets(top: 0, leading: hInset, bottom: 0, trailing: hInset))
stackView.addArrangedSubview(quoteViewContainer)
}
// Body label
let bodyLabel = VisibleMessageCell.getBodyLabel(for: viewItem, with: bodyLabelTextColor)
stackView.addArrangedSubview(bodyLabel)
// Body text view
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: bodyLabelTextColor, delegate: self)
self.bodyTextView = bodyTextView
stackView.addArrangedSubview(bodyTextView)
// Constraints
snContentView.addSubview(stackView)
stackView.pin(to: snContentView, withInset: inset)
@ -305,6 +307,16 @@ final class VisibleMessageCell : MessageCell {
}
// MARK: Interaction
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let bodyTextView = bodyTextView {
let pointInBodyTextViewCoordinates = convert(point, to: bodyTextView)
if bodyTextView.bounds.contains(pointInBodyTextViewCoordinates) {
return bodyTextView
}
}
return super.hitTest(point, with: event)
}
@objc private func handleLongPress() {
guard let viewItem = viewItem else { return }
delegate?.handleViewItemLongPressed(viewItem)
@ -320,6 +332,11 @@ final class VisibleMessageCell : MessageCell {
delegate?.handleViewItemDoubleTapped(viewItem)
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
delegate?.openURL(URL)
return false
}
// MARK: Convenience
private func getCornersToRound() -> UIRectCorner {
guard !isOnlyMessageInCluster else { return .allCorners }
@ -409,15 +426,29 @@ final class VisibleMessageCell : MessageCell {
return isGroupThread && viewItem.shouldShowSenderProfilePicture && senderSessionID != nil
}
static func getBodyLabel(for viewItem: ConversationViewItem, with textColor: UIColor) -> UILabel {
static func getBodyTextView(for viewItem: ConversationViewItem, with availableWidth: CGFloat, textColor: UIColor, delegate: UITextViewDelegate) -> UITextView {
guard let message = viewItem.interaction as? TSMessage else { preconditionFailure() }
let isOutgoing = (message.interactionType() == .outgoingMessage)
let bodyLabel = UILabel()
bodyLabel.numberOfLines = 0
bodyLabel.lineBreakMode = .byWordWrapping
bodyLabel.textColor = textColor
bodyLabel.font = .systemFont(ofSize: getFontSize(for: viewItem))
bodyLabel.attributedText = given(message.body) { MentionUtilities.highlightMentions(in: $0, isOutgoingMessage: isOutgoing, threadID: viewItem.interaction.uniqueThreadId, attributes: [:]) }
return bodyLabel
let result = UITextView()
result.isEditable = false
let attributes: [NSAttributedString.Key:Any] = [
.foregroundColor : textColor,
.font : UIFont.systemFont(ofSize: getFontSize(for: viewItem))
]
result.attributedText = given(message.body) { MentionUtilities.highlightMentions(in: $0, isOutgoingMessage: isOutgoing, threadID: viewItem.interaction.uniqueThreadId, attributes: attributes) }
result.dataDetectorTypes = .link
result.backgroundColor = .clear
result.isOpaque = false
result.textContainerInset = UIEdgeInsets.zero
result.contentInset = UIEdgeInsets.zero
result.textContainer.lineFragmentPadding = 0
result.isScrollEnabled = false
result.isUserInteractionEnabled = true
result.delegate = delegate
result.linkTextAttributes = [ .foregroundColor : textColor, .underlineStyle : NSUnderlineStyle.single.rawValue ]
let availableSpace = CGSize(width: availableWidth, height: .greatestFiniteMagnitude)
let size = result.sizeThatFits(availableSpace)
result.set(.height, to: size.height)
return result
}
}

View File

@ -0,0 +1,67 @@
final class ScrollToBottomButton : UIView {
private let delegate: ScrollToBottomButtonDelegate
// MARK: Settings
private static let size: CGFloat = 40
private static let iconSize: CGFloat = 16
// MARK: Lifecycle
init(delegate: ScrollToBottomButtonDelegate) {
self.delegate = delegate
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(delegate:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(delegate:) instead.")
}
private func setUpViewHierarchy() {
// Background & blur
let backgroundView = UIView()
backgroundView.backgroundColor = isLightMode ? .white : .black
backgroundView.alpha = Values.lowOpacity
addSubview(backgroundView)
backgroundView.pin(to: self)
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
addSubview(blurView)
blurView.pin(to: self)
// Size & shape
let size = ScrollToBottomButton.size
set(.width, to: size)
set(.height, to: size)
layer.cornerRadius = size / 2
layer.masksToBounds = true
// Border
layer.borderWidth = 1
let borderColor = (isLightMode ? UIColor.black : UIColor.white).withAlphaComponent(Values.veryLowOpacity)
layer.borderColor = borderColor.cgColor
// Icon
let tint = isLightMode ? UIColor.black : UIColor.white
let icon = UIImage(named: "ic_chevron_down")!.withTint(tint)
let iconImageView = UIImageView(image: icon)
iconImageView.set(.width, to: ScrollToBottomButton.iconSize)
iconImageView.set(.height, to: ScrollToBottomButton.iconSize)
iconImageView.contentMode = .scaleAspectFit
addSubview(iconImageView)
iconImageView.center(in: self)
// Gesture recognizer
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tapGestureRecognizer)
}
// MARK: Interaction
@objc private func handleTap() {
delegate.handleScrollToBottomButtonTapped()
}
}
protocol ScrollToBottomButtonDelegate {
func handleScrollToBottomButtonTapped()
}

View File

@ -0,0 +1,69 @@
final class URLModal : Modal {
private let url: URL
// MARK: Lifecycle
init(url: URL) {
self.url = url
super.init(nibName: nil, bundle: nil)
}
override init(nibName: String?, bundle: Bundle?) {
preconditionFailure("Use init(url:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(url:) instead.")
}
override func populateContentView() {
// Title
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.largeFontSize)
titleLabel.text = "Open URL?"
titleLabel.textAlignment = .center
// Message
let messageLabel = UILabel()
messageLabel.textColor = Colors.text
messageLabel.font = .systemFont(ofSize: Values.smallFontSize)
let message = "Are you sure you want to open \(url.absoluteString)?"
let attributedMessage = NSMutableAttributedString(string: message)
attributedMessage.addAttributes([ .font : UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], range: (message as NSString).range(of: url.absoluteString))
messageLabel.attributedText = attributedMessage
messageLabel.numberOfLines = 0
messageLabel.lineBreakMode = .byWordWrapping
messageLabel.textAlignment = .center
// Open button
let openButton = UIButton()
openButton.set(.height, to: Values.mediumButtonHeight)
openButton.layer.cornerRadius = Values.modalButtonCornerRadius
openButton.backgroundColor = Colors.buttonBackground
openButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
openButton.setTitleColor(Colors.text, for: UIControl.State.normal)
openButton.setTitle("Open", for: UIControl.State.normal)
openButton.addTarget(self, action: #selector(openURL), for: UIControl.Event.touchUpInside)
// Button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, openButton ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.mediumSpacing
buttonStackView.distribution = .fillEqually
// Main stack view
let mainStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel, buttonStackView ])
mainStackView.axis = .vertical
mainStackView.spacing = Values.largeSpacing
contentView.addSubview(mainStackView)
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing)
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: Values.largeSpacing)
}
// MARK: Interaction
@objc private func openURL() {
let url = self.url
presentingViewController?.dismiss(animated: true, completion: {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
})
}
}