From 11233f7d25f202a311cdcb6be247be9a55e79d27 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 4 Nov 2022 21:32:49 -0400 Subject: [PATCH] Dyanmic type support in profile header view --- Tusker.xcodeproj/project.pbxproj | 4 + Tusker/Views/LinkTextView.swift | 14 +- .../Profile Header/ProfileFieldsView.swift | 160 ++++++++++++++++++ .../Profile Header/ProfileHeaderView.swift | 56 ++---- .../Profile Header/ProfileHeaderView.xib | 60 +++---- .../Status/StatusMetaIndicatorsView.swift | 14 +- 6 files changed, 229 insertions(+), 79 deletions(-) create mode 100644 Tusker/Views/Profile Header/ProfileFieldsView.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 33b7489f..a7170b48 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -125,6 +125,7 @@ D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; }; D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; }; D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; }; + D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; }; D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; }; D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; }; D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; }; @@ -479,6 +480,7 @@ D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = ""; }; D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = ""; }; + D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldsView.swift; sourceTree = ""; }; D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = ""; }; D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = ""; }; D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentViewController.swift; sourceTree = ""; }; @@ -1061,6 +1063,7 @@ children = ( D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */, D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */, + D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */, ); path = "Profile Header"; sourceTree = ""; @@ -1816,6 +1819,7 @@ D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */, D620483623D38075008A63EF /* ContentTextView.swift in Sources */, + D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */, D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */, D6A57408255C53EC00674551 /* ComposeTextViewCaretScrolling.swift in Sources */, D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */, diff --git a/Tusker/Views/LinkTextView.swift b/Tusker/Views/LinkTextView.swift index 69defc8d..f7dbad71 100644 --- a/Tusker/Views/LinkTextView.swift +++ b/Tusker/Views/LinkTextView.swift @@ -9,10 +9,18 @@ import UIKit class LinkTextView: UITextView { + + override init(frame: CGRect, textContainer: NSTextContainer?) { + super.init(frame: frame, textContainer: textContainer) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } - override func awakeFromNib() { - super.awakeFromNib() - + private func commonInit() { delaysContentTouches = false isScrollEnabled = false isEditable = false diff --git a/Tusker/Views/Profile Header/ProfileFieldsView.swift b/Tusker/Views/Profile Header/ProfileFieldsView.swift new file mode 100644 index 00000000..b75e4f8e --- /dev/null +++ b/Tusker/Views/Profile Header/ProfileFieldsView.swift @@ -0,0 +1,160 @@ +// +// ProfileFieldsView.swift +// Tusker +// +// Created by Shadowfacts on 11/4/22. +// Copyright © 2022 Shadowfacts. All rights reserved. +// + +import UIKit + +class ProfileFieldsView: UIView { + + weak var delegate: ProfileHeaderViewDelegate? + + private let stack = UIStackView() + private var fieldViews: [(EmojiLabel, ContentTextView)] = [] + private var fieldConstraints: [NSLayoutConstraint] = [] + + private var isUsingSingleColumn: Bool = false + private var needsSingleColumn: Bool { + traitCollection.preferredContentSizeCategory > .large + } + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + stack.axis = .vertical + stack.alignment = .fill + stack.translatesAutoresizingMaskIntoConstraints = false + addSubview(stack) + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalTo: leadingAnchor), + stack.trailingAnchor.constraint(equalTo: trailingAnchor), + stack.topAnchor.constraint(equalTo: topAnchor), + stack.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if isUsingSingleColumn != needsSingleColumn { + configureFields() + } + } + + func updateUI(account: AccountMO) { + isHidden = account.fields.isEmpty + guard !account.fields.isEmpty else { + return + } + + for (name, value) in fieldViews { + name.removeFromSuperview() + value.removeFromSuperview() + } + fieldViews = [] + + for field in account.fields { + let nameLabel = EmojiLabel() + nameLabel.text = field.name + nameLabel.font = .preferredFont(forTextStyle: .body).withTraits(.traitBold)! + nameLabel.adjustsFontForContentSizeCategory = true + nameLabel.numberOfLines = 0 + nameLabel.lineBreakMode = .byWordWrapping + nameLabel.setEmojis(account.emojis, identifier: account.id) + nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + + let valueTextView = ContentTextView() + valueTextView.isSelectable = false + valueTextView.defaultFont = .preferredFont(forTextStyle: .body) + valueTextView.adjustsFontForContentSizeCategory = true + valueTextView.setTextFromHtml(field.value) + valueTextView.setEmojis(account.emojis, identifier: account.id) + valueTextView.navigationDelegate = delegate + valueTextView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + + fieldViews.append((nameLabel, valueTextView)) + } + + configureFields() + } + + @objc private func configureFields() { + guard !isHidden else { + return + } + + isUsingSingleColumn = needsSingleColumn + + NSLayoutConstraint.deactivate(fieldConstraints) + fieldConstraints = [] + + stack.arrangedSubviews.forEach { $0.removeFromSuperview() } + + if needsSingleColumn { + stack.spacing = 4 + var isFirst = true + for (name, value) in fieldViews { + if isFirst { + isFirst = false + } else { + let spacer = UIView() + // don't need any height, since there's 4pts of padding on either side + spacer.heightAnchor.constraint(equalToConstant: 0).isActive = true + stack.addArrangedSubview(spacer) + } + name.textAlignment = .natural + stack.addArrangedSubview(name) + value.textAlignment = .natural + stack.addArrangedSubview(value) + } + } else { + stack.spacing = 8 + + let dividerLayoutGuide = UILayoutGuide() + addLayoutGuide(dividerLayoutGuide) + fieldConstraints.append(contentsOf: [ + dividerLayoutGuide.widthAnchor.constraint(equalToConstant: 8), + ]) + + for (name, value) in fieldViews { + name.textAlignment = .right + name.translatesAutoresizingMaskIntoConstraints = false + + value.textAlignment = .left + value.translatesAutoresizingMaskIntoConstraints = false + + let fieldContainer = UIView() + fieldContainer.addSubview(name) + fieldContainer.addSubview(value) + stack.addArrangedSubview(fieldContainer) + fieldConstraints.append(contentsOf: [ + name.leadingAnchor.constraint(equalTo: fieldContainer.leadingAnchor), + name.trailingAnchor.constraint(equalTo: dividerLayoutGuide.leadingAnchor), + name.topAnchor.constraint(equalTo: fieldContainer.topAnchor), + name.bottomAnchor.constraint(equalTo: fieldContainer.bottomAnchor), + + value.leadingAnchor.constraint(equalTo: dividerLayoutGuide.trailingAnchor), + value.trailingAnchor.constraint(equalTo: fieldContainer.trailingAnchor), + value.topAnchor.constraint(equalTo: fieldContainer.topAnchor), + value.bottomAnchor.constraint(equalTo: fieldContainer.bottomAnchor), + + name.widthAnchor.constraint(greaterThanOrEqualTo: value.widthAnchor, multiplier: 0.5), + name.widthAnchor.constraint(lessThanOrEqualTo: value.widthAnchor, multiplier: 2), + ]) + } + } + + NSLayoutConstraint.activate(fieldConstraints) + } + +} diff --git a/Tusker/Views/Profile Header/ProfileHeaderView.swift b/Tusker/Views/Profile Header/ProfileHeaderView.swift index b6659d24..1004ca58 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderView.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderView.swift @@ -36,9 +36,7 @@ class ProfileHeaderView: UIView { @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var followsYouLabel: UILabel! @IBOutlet weak var noteTextView: StatusContentTextView! - @IBOutlet weak var fieldsStackView: UIStackView! - @IBOutlet weak var fieldNamesStackView: UIStackView! - @IBOutlet weak var fieldValuesStackView: UIStackView! + @IBOutlet weak var fieldsView: ProfileFieldsView! @IBOutlet weak var pagesSegmentedControl: UISegmentedControl! var accountID: String! @@ -68,12 +66,23 @@ class ProfileHeaderView: UIView { moreButton.layer.cornerRadius = 16 moreButton.layer.masksToBounds = true - - NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) - moreButton.addInteraction(UIPointerInteraction(delegate: self)) moreButton.showsMenuAsPrimaryAction = true moreButton.isContextMenuInteractionEnabled = true + + displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 24, weight: .semibold)) + displayNameLabel.adjustsFontForContentSizeCategory = true + + usernameLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .light)) + usernameLabel.adjustsFontForContentSizeCategory = true + + followsYouLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14)) + followsYouLabel.adjustsFontForContentSizeCategory = true + + noteTextView.defaultFont = .preferredFont(forTextStyle: .body) + noteTextView.adjustsFontForContentSizeCategory = true + + NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) } private func createObservers() { @@ -128,43 +137,14 @@ class ProfileHeaderView: UIView { } } - fieldsStackView.isHidden = account.fields.isEmpty - - fieldNamesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - fieldValuesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - var fieldAccessibilityElements = [Any]() - for field in account.fields { - let nameLabel = EmojiLabel() - nameLabel.text = field.name - nameLabel.font = .boldSystemFont(ofSize: 17) - nameLabel.textAlignment = .right - nameLabel.numberOfLines = 0 - nameLabel.lineBreakMode = .byWordWrapping - nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - nameLabel.setEmojis(account.emojis, identifier: "") - fieldNamesStackView.addArrangedSubview(nameLabel) - - let valueTextView = ContentTextView() - valueTextView.isSelectable = false - valueTextView.font = .systemFont(ofSize: 17) - valueTextView.setTextFromHtml(field.value) - valueTextView.setEmojis(account.emojis, identifier: account.id) - valueTextView.textAlignment = .left - valueTextView.awakeFromNib() - valueTextView.navigationDelegate = delegate - valueTextView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - fieldValuesStackView.addArrangedSubview(valueTextView) - - nameLabel.heightAnchor.constraint(equalTo: valueTextView.heightAnchor).isActive = true - fieldAccessibilityElements.append(nameLabel) - fieldAccessibilityElements.append(valueTextView) - } + fieldsView.updateUI(account: account) accessibilityElements = [ displayNameLabel!, usernameLabel!, noteTextView!, - ] + fieldAccessibilityElements + [ + // TODO: voiceover for fieldsview + // fieldsView!, moreButton!, pagesSegmentedControl!, ] diff --git a/Tusker/Views/Profile Header/ProfileHeaderView.xib b/Tusker/Views/Profile Header/ProfileHeaderView.xib index 817bef3e..3a470f87 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderView.xib +++ b/Tusker/Views/Profile Header/ProfileHeaderView.xib @@ -1,8 +1,9 @@ - + - + + @@ -14,13 +15,13 @@ - + - + @@ -38,14 +39,14 @@ -