This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended. To summarise this work: - [x] created a new **Xcode** project; - [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project; - [x] created the `Locations` app and also, its `Libraries` package; - [x] created the `Shared` package to share dependencies between the apps; - [x] added a `Makefile` file and implemented some **environment** and **help** commands. Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Reviewed-on: rock-n-code/deep-linking-assignment#1
207 lines
7.6 KiB
Swift
207 lines
7.6 KiB
Swift
import UIKit
|
|
|
|
protocol OldTalkPageHeaderViewDelegate: AnyObject {
|
|
func tappedLink(_ url: URL, headerView: OldTalkPageHeaderView, sourceView: UIView, sourceRect: CGRect?)
|
|
func tappedIntro(headerView: OldTalkPageHeaderView)
|
|
}
|
|
|
|
class OldTalkPageHeaderView: UIView {
|
|
|
|
weak var delegate: OldTalkPageHeaderViewDelegate?
|
|
|
|
struct ViewModel {
|
|
let header: String
|
|
let title: String
|
|
let info: String?
|
|
let intro: String?
|
|
}
|
|
|
|
@IBOutlet private var headerLabel: UILabel!
|
|
@IBOutlet private(set) var titleTextView: UITextView!
|
|
@IBOutlet private(set) var infoLabel: UILabel!
|
|
@IBOutlet private var introTextView: UITextView!
|
|
|
|
private var viewModel: ViewModel?
|
|
|
|
private var theme: Theme?
|
|
|
|
private var hasInfoText: Bool {
|
|
return viewModel?.info != nil
|
|
}
|
|
|
|
private var hasIntroText: Bool {
|
|
return viewModel?.intro != nil
|
|
}
|
|
|
|
private var hasTitleText: Bool {
|
|
if let viewModel = viewModel {
|
|
return viewModel.title.count > 0
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
var semanticContentAttributeOverride: UISemanticContentAttribute = .unspecified {
|
|
didSet {
|
|
textAlignmentOverride = semanticContentAttributeOverride == .forceRightToLeft ? NSTextAlignment.right : NSTextAlignment.left
|
|
|
|
headerLabel.semanticContentAttribute = semanticContentAttributeOverride
|
|
titleTextView.semanticContentAttribute = semanticContentAttributeOverride
|
|
infoLabel.semanticContentAttribute = semanticContentAttributeOverride
|
|
introTextView.semanticContentAttribute = semanticContentAttributeOverride
|
|
}
|
|
}
|
|
|
|
private var textAlignmentOverride: NSTextAlignment = .left {
|
|
didSet {
|
|
headerLabel.textAlignment = textAlignmentOverride
|
|
titleTextView.textAlignment = textAlignmentOverride
|
|
infoLabel.textAlignment = textAlignmentOverride
|
|
introTextView.textAlignment = textAlignmentOverride
|
|
}
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
assertionFailure("init(frame) not setup for TalkPageHeaderView")
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
super.init(coder: aDecoder)
|
|
}
|
|
|
|
override func awakeFromNib() {
|
|
super.awakeFromNib()
|
|
setup()
|
|
}
|
|
|
|
func setup() {
|
|
infoLabel.numberOfLines = 0
|
|
titleTextView.isEditable = false
|
|
titleTextView.isScrollEnabled = false
|
|
titleTextView.delegate = self
|
|
titleTextView.textContainerInset = UIEdgeInsets.zero
|
|
titleTextView.textContainer.lineFragmentPadding = 0
|
|
introTextView.isEditable = false
|
|
introTextView.isScrollEnabled = false
|
|
introTextView.delegate = self
|
|
introTextView.textContainer.maximumNumberOfLines = 3
|
|
introTextView.textContainer.lineBreakMode = .byTruncatingTail
|
|
introTextView.textContainerInset = UIEdgeInsets.zero
|
|
introTextView.textContainer.lineFragmentPadding = 0
|
|
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tappedIntro(sender:)))
|
|
introTextView.addGestureRecognizer(tapGestureRecognizer)
|
|
headerLabel.accessibilityTraits = .header
|
|
titleTextView.accessibilityTraits = .header
|
|
updateFonts(with: traitCollection)
|
|
}
|
|
|
|
func configure(viewModel: ViewModel) {
|
|
|
|
self.viewModel = viewModel
|
|
|
|
if hasInfoText {
|
|
infoLabel.text = viewModel.info
|
|
introTextView.isHidden = false
|
|
} else {
|
|
infoLabel.isHidden = true
|
|
}
|
|
|
|
headerLabel.text = viewModel.header
|
|
|
|
if hasTitleText {
|
|
let titleAttributedString = viewModel.title.byAttributingHTML(with: .boldTitle1, boldWeight: .bold, matching: traitCollection, color: titleTextView.textColor, linkColor: theme?.colors.link, handlingSuperSubscripts: true)
|
|
titleTextView.attributedText = titleAttributedString
|
|
titleTextView.isHidden = false
|
|
} else {
|
|
titleTextView.isHidden = true
|
|
}
|
|
|
|
if let intro = viewModel.intro {
|
|
introTextView.isHidden = false
|
|
setupIntro(text: intro)
|
|
} else {
|
|
introTextView.isHidden = true
|
|
}
|
|
}
|
|
|
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
guard !UIAccessibility.isVoiceOverRunning else {
|
|
return super.point(inside: point, with: event)
|
|
}
|
|
|
|
let titleConvertedPoint = self.convert(point, to: titleTextView)
|
|
if titleTextView.point(inside: titleConvertedPoint, with: event) {
|
|
return true
|
|
}
|
|
|
|
let introConvertedPoint = self.convert(point, to: introTextView)
|
|
if introTextView.point(inside: introConvertedPoint, with: event) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
private func setupIntro(text: String) {
|
|
introTextView.attributedText = text.byAttributingHTML(with: .footnote, boldWeight: .semibold, matching: traitCollection, color: introTextView.textColor, linkColor: theme?.colors.link, handlingLists: true, handlingSuperSubscripts: true, tagMapping: ["a": "b"])
|
|
}
|
|
|
|
@objc private func tappedIntro(sender: UITextView) {
|
|
delegate?.tappedIntro(headerView: self)
|
|
}
|
|
|
|
// MARK: - Dynamic Type
|
|
// Only applies new fonts if the content size category changes
|
|
|
|
open override func setNeedsLayout() {
|
|
maybeUpdateFonts(with: traitCollection)
|
|
super.setNeedsLayout()
|
|
}
|
|
|
|
override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
setNeedsLayout()
|
|
}
|
|
|
|
var contentSizeCategory: UIContentSizeCategory?
|
|
fileprivate func maybeUpdateFonts(with traitCollection: UITraitCollection) {
|
|
guard contentSizeCategory == nil || contentSizeCategory != traitCollection.wmf_preferredContentSizeCategory else {
|
|
return
|
|
}
|
|
contentSizeCategory = traitCollection.wmf_preferredContentSizeCategory
|
|
updateFonts(with: traitCollection)
|
|
}
|
|
|
|
func updateFonts(with traitCollection: UITraitCollection) {
|
|
headerLabel.font = UIFont.wmf_font(DynamicTextStyle.semiboldFootnote, compatibleWithTraitCollection: traitCollection)
|
|
titleTextView.font = UIFont.wmf_font(DynamicTextStyle.boldTitle1, compatibleWithTraitCollection: traitCollection)
|
|
infoLabel.font = UIFont.wmf_font(DynamicTextStyle.footnote, compatibleWithTraitCollection: traitCollection)
|
|
if let intro = viewModel?.intro {
|
|
setupIntro(text: intro)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension OldTalkPageHeaderView: Themeable {
|
|
func apply(theme: Theme) {
|
|
self.theme = theme
|
|
titleTextView.backgroundColor = theme.colors.paperBackground
|
|
headerLabel.textColor = theme.colors.secondaryText
|
|
titleTextView.textColor = theme.colors.primaryText
|
|
infoLabel.textColor = theme.colors.secondaryText
|
|
introTextView.textColor = theme.colors.primaryText
|
|
introTextView.backgroundColor = theme.colors.paperBackground
|
|
backgroundColor = theme.colors.paperBackground
|
|
}
|
|
}
|
|
|
|
// MARK: UITextViewDelegate
|
|
|
|
extension OldTalkPageHeaderView: UITextViewDelegate {
|
|
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
|
delegate?.tappedLink(URL, headerView: self, sourceView: textView, sourceRect: textView.frame(of: characterRange))
|
|
return false
|
|
}
|
|
}
|