575 lines
22 KiB
Swift
575 lines
22 KiB
Swift
//
|
|
// Copyright 2024 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
import SignalServiceKit
|
|
public import SignalUI
|
|
|
|
public enum SafetyTipsType {
|
|
case contact
|
|
case group
|
|
}
|
|
|
|
public protocol SafetyTipsViewControllerDelegate: AnyObject {
|
|
func didTapViewMoreSafetyTips()
|
|
}
|
|
|
|
public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollViewDelegate {
|
|
private enum SafetyTips: CaseIterable {
|
|
case chatsFromSignal
|
|
case reviewNames
|
|
case scams
|
|
|
|
var image: UIImage? {
|
|
switch self {
|
|
case .chatsFromSignal:
|
|
return UIImage(resource: .safetytip4801)
|
|
case .reviewNames:
|
|
return UIImage(resource: .safetytip4802)
|
|
case .scams:
|
|
return UIImage(resource: .safetytip4803)
|
|
}
|
|
}
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .chatsFromSignal:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_SIGNAL_CHATS_TITLE",
|
|
comment: "Message title describing the signal chats tip.",
|
|
)
|
|
case .reviewNames:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_REVIEW_NAMES_TITLE",
|
|
comment: "Message title describing the review names safety tip.",
|
|
)
|
|
case .scams:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_LOOK_OUT_FOR_SCAMS_TITLE",
|
|
comment: "Message title describing the scams safety tip.",
|
|
)
|
|
}
|
|
}
|
|
|
|
var body: String {
|
|
switch self {
|
|
case .chatsFromSignal:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_SIGNAL_CHATS_BODY",
|
|
comment: "Message body describing the signal chats tip.",
|
|
)
|
|
case .reviewNames:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_REVIEW_NAMES_BODY",
|
|
comment: "Message body describing the review names safety tip.",
|
|
)
|
|
case .scams:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_LOOK_OUT_FOR_SCAMS_BODY",
|
|
comment: "Message body describing the scams safety tip.",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
let contentScrollView = UIScrollView()
|
|
let stackView = UIStackView()
|
|
|
|
public weak var delegate: SafetyTipsViewControllerDelegate?
|
|
|
|
override public func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
minimizedHeight = min(612, CurrentAppContext().frame.height)
|
|
super.allowsExpansion = false
|
|
|
|
let header = UILabel()
|
|
header.text = OWSLocalizedString(
|
|
"SAFETY_TIPS_HEADER_TITLE",
|
|
comment: "Title for Safety Tips education screen.",
|
|
)
|
|
header.font = .dynamicTypeHeadline
|
|
header.textAlignment = .center
|
|
header.isAccessibilityElement = true
|
|
header.accessibilityTraits.insert(.header)
|
|
contentView.addSubview(header)
|
|
header.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
header.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
|
header.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: 16),
|
|
])
|
|
|
|
contentView.addSubview(contentScrollView)
|
|
contentScrollView.addSubview(stackView)
|
|
|
|
stackView.axis = .vertical
|
|
stackView.spacing = 20
|
|
|
|
contentScrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
NSLayoutConstraint.activate([
|
|
contentScrollView.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 24),
|
|
contentScrollView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -90),
|
|
contentScrollView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
contentScrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
|
|
|
stackView.topAnchor.constraint(equalTo: contentScrollView.contentLayoutGuide.topAnchor),
|
|
stackView.bottomAnchor.constraint(equalTo: contentScrollView.contentLayoutGuide.bottomAnchor),
|
|
stackView.leadingAnchor.constraint(equalTo: contentScrollView.contentLayoutGuide.leadingAnchor, constant: 24),
|
|
stackView.trailingAnchor.constraint(equalTo: contentScrollView.contentLayoutGuide.trailingAnchor, constant: 24),
|
|
stackView.widthAnchor.constraint(equalTo: contentScrollView.frameLayoutGuide.widthAnchor, constant: -48),
|
|
])
|
|
|
|
for bullet in SafetyTips.allCases {
|
|
let bulletView = SafetyBulletView(bullet)
|
|
stackView.addArrangedSubview(bulletView)
|
|
}
|
|
|
|
var config = UIButton.Configuration.filled()
|
|
config.baseBackgroundColor = UIColor.Signal.secondaryFill
|
|
config.cornerStyle = .capsule
|
|
var attrString = AttributedString(CommonStrings.viewMoreButton)
|
|
attrString.font = .dynamicTypeBodyClamped.medium()
|
|
config.attributedTitle = attrString
|
|
config.baseForegroundColor = UIColor.Signal.label
|
|
config.contentInsets = .init(margin: 14)
|
|
let viewMoreButton = UIButton(
|
|
configuration: config,
|
|
primaryAction: .init(handler: { [weak self] _ in
|
|
self?.dismiss(animated: true)
|
|
self?.delegate?.didTapViewMoreSafetyTips()
|
|
}),
|
|
)
|
|
|
|
viewMoreButton.translatesAutoresizingMaskIntoConstraints = false
|
|
contentView.addSubview(viewMoreButton)
|
|
NSLayoutConstraint.activate([
|
|
viewMoreButton.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor),
|
|
viewMoreButton.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor, constant: 20),
|
|
viewMoreButton.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, constant: -20),
|
|
viewMoreButton.heightAnchor.constraint(equalToConstant: 52),
|
|
])
|
|
}
|
|
|
|
private class SafetyBulletView: UIStackView {
|
|
init(_ bullet: SafetyTips) {
|
|
super.init(frame: .zero)
|
|
|
|
self.axis = .horizontal
|
|
self.alignment = .firstBaseline
|
|
self.spacing = 24
|
|
self.isLayoutMarginsRelativeArrangement = true
|
|
self.layoutMargins = .zero
|
|
|
|
let textStack = UIStackView()
|
|
textStack.axis = .vertical
|
|
textStack.spacing = 8
|
|
|
|
let headerLabel = UILabel()
|
|
headerLabel.text = bullet.title
|
|
headerLabel.numberOfLines = 0
|
|
headerLabel.textColor = UIColor.Signal.label
|
|
headerLabel.font = .dynamicTypeBody.semibold()
|
|
textStack.addArrangedSubview(headerLabel)
|
|
|
|
let bodyLabel = UILabel()
|
|
bodyLabel.text = bullet.body
|
|
bodyLabel.numberOfLines = 0
|
|
bodyLabel.textColor = UIColor.Signal.secondaryLabel
|
|
bodyLabel.font = .dynamicTypeBody
|
|
textStack.addArrangedSubview(bodyLabel)
|
|
|
|
let bulletPoint = UIImageView(image: bullet.image)
|
|
bulletPoint.contentMode = .scaleAspectFit
|
|
bulletPoint.translatesAutoresizingMaskIntoConstraints = false
|
|
bulletPoint.widthAnchor.constraint(equalToConstant: 48).isActive = true
|
|
bulletPoint.heightAnchor.constraint(equalToConstant: 48).isActive = true
|
|
|
|
addArrangedSubview(bulletPoint)
|
|
addArrangedSubview(textStack)
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
}
|
|
|
|
public class MoreSafetyTipsViewController: InteractiveSheetViewController, UIScrollViewDelegate {
|
|
let contentScrollView = UIScrollView()
|
|
override public var interactiveScrollViews: [UIScrollView] { [contentScrollView] }
|
|
override public var sheetBackgroundColor: UIColor { Theme.tableView2PresentedBackgroundColor }
|
|
|
|
private enum Constants {
|
|
static let stackSpacing: CGFloat = 12.0
|
|
|
|
static let outerSpacing: CGFloat = 20.0
|
|
static let outerMargins: UIEdgeInsets = .init(hMargin: 24.0, vMargin: 0.0)
|
|
|
|
static let footerSpacing: CGFloat = 16.0
|
|
|
|
static let buttonDiameter: CGFloat = 52.0
|
|
static let buttonMargin: CGFloat = 24.0
|
|
}
|
|
|
|
fileprivate enum MoreSafetyTips: CaseIterable {
|
|
case chatsFromSignal
|
|
case reviewNames
|
|
case vagueMessages
|
|
case messagesWithLinks
|
|
case crypto
|
|
case fakeBusiness
|
|
|
|
var image: UIImage? {
|
|
switch self {
|
|
case .chatsFromSignal:
|
|
return UIImage(resource: .safetytip24001)
|
|
case .reviewNames:
|
|
return UIImage(resource: .safetytip24002)
|
|
case .vagueMessages:
|
|
return UIImage(resource: .safetytip24003)
|
|
case .messagesWithLinks:
|
|
return UIImage(resource: .safetytip24004)
|
|
case .crypto:
|
|
return UIImage(resource: .safetytip24005)
|
|
case .fakeBusiness:
|
|
return UIImage(resource: .safetytip24006)
|
|
}
|
|
}
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .chatsFromSignal:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_SIGNAL_CHATS_TITLE",
|
|
comment: "Message title describing the signal chats tip.",
|
|
)
|
|
case .reviewNames:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_REVIEW_NAMES_TITLE",
|
|
comment: "Message title describing the review names safety tip.",
|
|
)
|
|
case .vagueMessages:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_VAGUE_MESSAGE_TITLE",
|
|
comment: "Message title describing the safety tip about vague messages.",
|
|
)
|
|
case .messagesWithLinks:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_MESSAGE_LINKS_TITLE",
|
|
comment: "Message title describing the safety tip about unknown links in messages.",
|
|
)
|
|
case .crypto:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_CRYPTO_TITLE",
|
|
comment: "Message title describing the crypto safety tip.",
|
|
)
|
|
case .fakeBusiness:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_FAKE_BUSINESS_TITLE",
|
|
comment: "Message title describing the safety tip about unknown or fake businesses.",
|
|
)
|
|
}
|
|
}
|
|
|
|
var body: String {
|
|
switch self {
|
|
case .chatsFromSignal:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_SIGNAL_CHATS_BODY_VIEW_MORE",
|
|
comment: "Message body describing the signal chats tip in the 'view more' flow.",
|
|
)
|
|
case .reviewNames:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_REVIEW_NAMES_BODY_VIEW_MORE",
|
|
comment: "Message body describing the review names safety tip in the 'view more' flow.",
|
|
)
|
|
case .vagueMessages:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_VAGUE_MESSAGE_BODY",
|
|
comment: "Message contents for the vague message safety tip.",
|
|
)
|
|
case .messagesWithLinks:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_MESSAGE_LINKS_BODY",
|
|
comment: "Message contents for the unknown links in messages safety tip.",
|
|
)
|
|
case .crypto:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_CRYPTO_BODY",
|
|
comment: "Message contents for the crypto safety tip.",
|
|
)
|
|
case .fakeBusiness:
|
|
return OWSLocalizedString(
|
|
"SAFETY_TIPS_FAKE_BUSINESS_BODY",
|
|
comment: "Message contents for the safety tip concerning fake businesses.",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
var prefersNavigationBarHidden: Bool { true }
|
|
|
|
override public func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
minimizedHeight = min(510, CurrentAppContext().frame.height)
|
|
super.allowsExpansion = false
|
|
|
|
contentView.addSubview(contentScrollView)
|
|
contentScrollView.addSubview(tipScrollView)
|
|
|
|
tipScrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
tipScrollView.topAnchor.constraint(equalTo: contentScrollView.topAnchor),
|
|
tipScrollView.bottomAnchor.constraint(equalTo: contentScrollView.bottomAnchor),
|
|
tipScrollView.leadingAnchor.constraint(equalTo: contentScrollView.leadingAnchor),
|
|
tipScrollView.trailingAnchor.constraint(equalTo: contentScrollView.trailingAnchor),
|
|
tipScrollView.widthAnchor.constraint(equalTo: contentScrollView.frameLayoutGuide.widthAnchor),
|
|
])
|
|
|
|
contentScrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
contentScrollView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
contentScrollView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -84),
|
|
contentScrollView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
contentScrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
|
contentScrollView.widthAnchor.constraint(equalTo: contentView.widthAnchor),
|
|
])
|
|
|
|
buildContents()
|
|
updateButtonState()
|
|
setColorsForCurrentTheme()
|
|
}
|
|
|
|
override public func themeDidChange() {
|
|
super.themeDidChange()
|
|
buildContents()
|
|
updateButtonState()
|
|
setColorsForCurrentTheme()
|
|
}
|
|
|
|
// MARK: - Views
|
|
|
|
private lazy var tipScrollView: UIScrollView = {
|
|
let scrollView = UIScrollView(frame: .zero)
|
|
scrollView.isPagingEnabled = true
|
|
scrollView.showsHorizontalScrollIndicator = false
|
|
scrollView.delegate = self
|
|
return scrollView
|
|
}()
|
|
|
|
private lazy var pageControl: UIPageControl = {
|
|
let pageControl = UIPageControl()
|
|
pageControl.numberOfPages = MoreSafetyTips.allCases.count
|
|
pageControl.currentPage = 0
|
|
pageControl.addTarget(self, action: #selector(self.changePage), for: .valueChanged)
|
|
return pageControl
|
|
}()
|
|
|
|
private lazy var previousTipButton: UIButton = {
|
|
let previousButton = UIButton(type: .system)
|
|
var config = UIButton.Configuration.filled()
|
|
config.baseForegroundColor = UIColor.Signal.label
|
|
config.baseBackgroundColor = UIColor.Signal.primaryFill
|
|
config.image = UIImage(resource: .chevronLeft26)
|
|
config.cornerStyle = .capsule
|
|
previousButton.accessibilityLabel = CommonStrings.backButton
|
|
previousButton.configuration = config
|
|
previousButton.addTarget(self, action: #selector(didTapPrevious), for: .touchUpInside)
|
|
|
|
return previousButton
|
|
|
|
}()
|
|
|
|
private lazy var nextTipButton: UIButton = {
|
|
let nextButton = UIButton(type: .system)
|
|
var config = UIButton.Configuration.filled()
|
|
config.baseForegroundColor = UIColor.Signal.label
|
|
config.baseBackgroundColor = UIColor.Signal.primaryFill
|
|
config.image = UIImage(resource: .chevronRight26)
|
|
config.cornerStyle = .capsule
|
|
nextButton.configuration = config
|
|
nextButton.accessibilityLabel = CommonStrings.nextButton
|
|
nextButton.addTarget(self, action: #selector(didTapNext), for: .touchUpInside)
|
|
|
|
return nextButton
|
|
}()
|
|
|
|
private lazy var footerView: UIView = {
|
|
let stackView = UIStackView(arrangedSubviews: [
|
|
previousTipButton,
|
|
pageControl,
|
|
nextTipButton,
|
|
])
|
|
|
|
nextTipButton.translatesAutoresizingMaskIntoConstraints = false
|
|
previousTipButton.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
nextTipButton.widthAnchor.constraint(equalToConstant: Constants.buttonDiameter),
|
|
nextTipButton.heightAnchor.constraint(equalToConstant: Constants.buttonDiameter),
|
|
previousTipButton.widthAnchor.constraint(equalToConstant: Constants.buttonDiameter),
|
|
previousTipButton.heightAnchor.constraint(equalToConstant: Constants.buttonDiameter),
|
|
])
|
|
|
|
let container = UIView()
|
|
container.addSubview(stackView)
|
|
container.backgroundColor = sheetBackgroundColor
|
|
container.tintColor = sheetBackgroundColor
|
|
|
|
stackView.axis = .horizontal
|
|
stackView.spacing = Constants.footerSpacing
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
stackView.centerXAnchor.constraint(equalTo: container.centerXAnchor),
|
|
stackView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -Constants.buttonMargin),
|
|
stackView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Constants.buttonMargin),
|
|
stackView.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -Constants.buttonMargin),
|
|
])
|
|
|
|
return container
|
|
}()
|
|
|
|
// MARK: - TableView
|
|
|
|
private func buildContents() {
|
|
prepareTipsScrollView()
|
|
|
|
contentView.addSubview(footerView)
|
|
footerView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
footerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
|
footerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
footerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
|
footerView.heightAnchor.constraint(equalToConstant: 84),
|
|
])
|
|
}
|
|
|
|
private func prepareTipsScrollView() {
|
|
var priorView: UIView?
|
|
tipScrollView.removeAllSubviews()
|
|
MoreSafetyTips.allCases.forEach { tip in
|
|
let view = SafetyTipView(safetyTip: tip)
|
|
tipScrollView.addSubview(view)
|
|
|
|
view.autoPinHeight(toHeightOf: tipScrollView)
|
|
view.autoPinWidth(toWidthOf: tipScrollView)
|
|
if let priorView {
|
|
view.autoPinEdge(.leading, to: .trailing, of: priorView)
|
|
} else {
|
|
view.autoPinEdge(.leading, to: .leading, of: tipScrollView)
|
|
}
|
|
priorView = view
|
|
}
|
|
priorView?.autoPinEdge(.trailing, to: .trailing, of: tipScrollView)
|
|
}
|
|
|
|
// MARK: - ScrollViewDelegate
|
|
|
|
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
let pageNumber = round(scrollView.contentOffset.x / scrollView.frame.size.width)
|
|
pageControl.currentPage = Int(pageNumber)
|
|
updateButtonState()
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
@objc
|
|
private func changePage() {
|
|
let x = CGFloat(pageControl.currentPage) * tipScrollView.frame.size.width
|
|
tipScrollView.setContentOffset(CGPoint(x: x, y: 0), animated: true)
|
|
updateButtonState()
|
|
|
|
let currentPageView = tipScrollView.subviews[pageControl.currentPage]
|
|
DispatchQueue.main.async {
|
|
UIAccessibility.post(notification: .layoutChanged, argument: currentPageView)
|
|
}
|
|
}
|
|
|
|
@objc
|
|
private func didTapPrevious() {
|
|
pageControl.currentPage = max(pageControl.currentPage - 1, 0)
|
|
changePage()
|
|
}
|
|
|
|
@objc
|
|
private func didTapNext() {
|
|
pageControl.currentPage = min(pageControl.currentPage + 1, pageControl.numberOfPages)
|
|
changePage()
|
|
}
|
|
|
|
private func updateButtonState() {
|
|
switch pageControl.currentPage {
|
|
case 0:
|
|
// hide previous, show next
|
|
previousTipButton.alpha = 0
|
|
previousTipButton.isUserInteractionEnabled = false
|
|
nextTipButton.alpha = 1
|
|
nextTipButton.isUserInteractionEnabled = true
|
|
case pageControl.numberOfPages - 1:
|
|
// show previous, hide next
|
|
previousTipButton.alpha = 1
|
|
previousTipButton.isUserInteractionEnabled = true
|
|
nextTipButton.alpha = 0
|
|
nextTipButton.isUserInteractionEnabled = false
|
|
default:
|
|
// show previous, show next
|
|
previousTipButton.alpha = 1
|
|
previousTipButton.isUserInteractionEnabled = true
|
|
nextTipButton.alpha = 1
|
|
nextTipButton.isUserInteractionEnabled = true
|
|
}
|
|
}
|
|
|
|
private func setColorsForCurrentTheme() {
|
|
pageControl.pageIndicatorTintColor = Theme.isDarkThemeEnabled ? .ows_gray65 : .ows_gray20
|
|
pageControl.currentPageIndicatorTintColor = Theme.isDarkThemeEnabled ? .ows_gray20 : .ows_gray65
|
|
}
|
|
}
|
|
|
|
extension MoreSafetyTipsViewController {
|
|
class SafetyTipView: UIView {
|
|
fileprivate init(safetyTip: MoreSafetyTips) {
|
|
super.init(frame: .zero)
|
|
layoutMargins = .init(hMargin: 24.0, vMargin: 0.0)
|
|
|
|
let stackView = UIStackView()
|
|
stackView.axis = .vertical
|
|
self.addSubview(stackView)
|
|
stackView.spacing = 12.0
|
|
stackView.autoPinEdgesToSuperviewMargins()
|
|
|
|
let imageView = UIImageView(image: safetyTip.image)
|
|
imageView.contentMode = .scaleAspectFit
|
|
|
|
stackView.addArrangedSubview(imageView)
|
|
|
|
let titleLabel = UILabel()
|
|
titleLabel.text = safetyTip.title
|
|
titleLabel.numberOfLines = 0
|
|
titleLabel.textAlignment = .center
|
|
titleLabel.lineBreakMode = .byWordWrapping
|
|
titleLabel.font = .dynamicTypeBody.bold()
|
|
titleLabel.textColor = Theme.primaryTextColor
|
|
stackView.addArrangedSubview(titleLabel)
|
|
|
|
let messageLabel = UILabel()
|
|
messageLabel.text = safetyTip.body
|
|
messageLabel.numberOfLines = 0
|
|
messageLabel.textAlignment = .center
|
|
messageLabel.lineBreakMode = .byWordWrapping
|
|
messageLabel.font = .dynamicTypeBodyClamped
|
|
messageLabel.textColor = Theme.secondaryTextAndIconColor
|
|
stackView.addArrangedSubview(messageLabel)
|
|
}
|
|
|
|
@available(*, unavailable, message: "Use other constructor")
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
}
|