Remove ambiguating constraint priorities, avoid removing and recreating the same constraints

Closes #407
This commit is contained in:
Shadowfacts 2023-07-05 20:30:55 -07:00
parent 50bfaf7236
commit 3c9692d5b2
7 changed files with 59 additions and 61 deletions

View File

@ -48,11 +48,7 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
$0.axis = .horizontal $0.axis = .horizontal
$0.alignment = .fill $0.alignment = .fill
$0.spacing = 8 $0.spacing = 8
let heightConstraint = $0.heightAnchor.constraint(equalToConstant: 30) $0.heightAnchor.constraint(equalToConstant: 30).isActive = true
// the collection view cell imposes a height constraint before it's calculated the actual height
// so let this constraint be broken temporarily to avoid unsatisfiable constraints log spam
heightConstraint.priority = .init(999)
heightConstraint.isActive = true
} }
private lazy var actionLabel = MultiSourceEmojiLabel().configure { private lazy var actionLabel = MultiSourceEmojiLabel().configure {

View File

@ -45,9 +45,7 @@ class FollowNotificationGroupCollectionViewCell: UICollectionViewListCell {
]).configure { ]).configure {
$0.axis = .horizontal $0.axis = .horizontal
$0.alignment = .fill $0.alignment = .fill
let heightConstraint = $0.heightAnchor.constraint(equalToConstant: 30) $0.heightAnchor.constraint(equalToConstant: 30).isActive = true
heightConstraint.priority = .init(999)
heightConstraint.isActive = true
} }
private lazy var actionLabel = MultiSourceEmojiLabel().configure { private lazy var actionLabel = MultiSourceEmojiLabel().configure {

View File

@ -48,9 +48,7 @@ class FollowRequestNotificationCollectionViewCell: UICollectionViewListCell {
]).configure { ]).configure {
$0.axis = .horizontal $0.axis = .horizontal
$0.alignment = .fill $0.alignment = .fill
let heightConstraint = $0.heightAnchor.constraint(equalToConstant: 30) $0.heightAnchor.constraint(equalToConstant: 30).isActive = true
heightConstraint.priority = .init(999)
heightConstraint.isActive = true
} }
private lazy var actionLabel = EmojiLabel().configure { private lazy var actionLabel = EmojiLabel().configure {

View File

@ -91,9 +91,6 @@ class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell {
contentView.addSubview(iconView) contentView.addSubview(iconView)
vStack.translatesAutoresizingMaskIntoConstraints = false vStack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(vStack) contentView.addSubview(vStack)
let vStackBottomConstraint = vStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8)
// need something to break during intermediate layouts when the cell imposes a 44pt height :S
vStackBottomConstraint.priority = .init(999)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
iconView.topAnchor.constraint(equalTo: vStack.topAnchor), iconView.topAnchor.constraint(equalTo: vStack.topAnchor),
iconView.trailingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16 + 50), iconView.trailingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16 + 50),
@ -101,7 +98,7 @@ class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell {
vStack.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8), vStack.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8),
vStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), vStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
vStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), vStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
vStackBottomConstraint, vStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
]) ])
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)

View File

@ -56,13 +56,8 @@ class PollOptionView: UIView {
percentLabel.setContentHuggingPriority(.required, for: .horizontal) percentLabel.setContentHuggingPriority(.required, for: .horizontal)
addSubview(percentLabel) addSubview(percentLabel)
let minHeightConstraint = heightAnchor.constraint(greaterThanOrEqualToConstant: PollOptionView.minHeight)
// on the first layout, something is weird and this becomes ambiguous even though it's fine on subsequent layouts
// this keeps autolayout from complaining
minHeightConstraint.priority = .required - 1
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
minHeightConstraint, heightAnchor.constraint(greaterThanOrEqualToConstant: PollOptionView.minHeight),
label.topAnchor.constraint(equalTo: topAnchor, constant: 4), label.topAnchor.constraint(equalTo: topAnchor, constant: 4),
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4), label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),

View File

@ -50,8 +50,9 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: StatusConte
private var isHiddenObservations: [NSKeyValueObservation] = [] private var isHiddenObservations: [NSKeyValueObservation] = []
private var visibleSubviews = IndexSet()
private var verticalConstraints: [NSLayoutConstraint] = [] private var verticalConstraints: [NSLayoutConstraint] = []
private var lastSubviewBottomConstraint: NSLayoutConstraint? private var lastSubviewBottomConstraint: (UIView, NSLayoutConstraint)?
private var zeroHeightConstraint: NSLayoutConstraint! private var zeroHeightConstraint: NSLayoutConstraint!
private var isCollapsed = false private var isCollapsed = false
@ -93,31 +94,35 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: StatusConte
} }
override func updateConstraints() { override func updateConstraints() {
NSLayoutConstraint.deactivate(verticalConstraints) let visibleSubviews = IndexSet(arrangedSubviews.indices.filter { !arrangedSubviews[$0].isHidden })
verticalConstraints = [] if self.visibleSubviews != visibleSubviews {
var lastVisibleSubview: UIView? self.visibleSubviews = visibleSubviews
NSLayoutConstraint.deactivate(verticalConstraints)
for subview in arrangedSubviews { verticalConstraints = []
guard !subview.isHidden else { var lastVisibleSubview: UIView?
continue
for subviewIndex in visibleSubviews {
let subview = arrangedSubviews[subviewIndex]
if let lastVisibleSubview {
verticalConstraints.append(subview.topAnchor.constraint(equalTo: lastVisibleSubview.bottomAnchor, constant: 4))
} else {
verticalConstraints.append(subview.topAnchor.constraint(equalTo: topAnchor))
}
lastVisibleSubview = subview
} }
if let lastVisibleSubview { NSLayoutConstraint.activate(verticalConstraints)
verticalConstraints.append(subview.topAnchor.constraint(equalTo: lastVisibleSubview.bottomAnchor, constant: 4)) }
} else {
verticalConstraints.append(subview.topAnchor.constraint(equalTo: topAnchor)) if lastSubviewBottomConstraint == nil || arrangedSubviews[visibleSubviews.last!] !== lastSubviewBottomConstraint?.0 {
} lastSubviewBottomConstraint?.1.isActive = false
// this constraint needs to have low priority so that during the collapse/expand animation, the content container is the view that shrinks/expands
lastVisibleSubview = subview let lastVisibleSubview = arrangedSubviews[visibleSubviews.last!]
let constraint = lastVisibleSubview.bottomAnchor.constraint(equalTo: bottomAnchor)
constraint.isActive = !isCollapsed
constraint.priority = .defaultLow
lastSubviewBottomConstraint = (lastVisibleSubview, constraint)
} }
NSLayoutConstraint.activate(verticalConstraints)
lastSubviewBottomConstraint?.isActive = false
// this constraint needs to have low priority so that during the collapse/expand animation, the content container is the view that shrinks/expands
lastSubviewBottomConstraint = subviews.last(where: { !$0.isHidden })!.bottomAnchor.constraint(equalTo: bottomAnchor)
lastSubviewBottomConstraint!.isActive = !isCollapsed
lastSubviewBottomConstraint!.priority = .defaultLow
zeroHeightConstraint.isActive = isCollapsed zeroHeightConstraint.isActive = isCollapsed
@ -133,7 +138,7 @@ class StatusContentContainer<ContentView: ContentTextView, PollView: StatusConte
// don't call setNeedsUpdateConstraints b/c that destroys/recreates a bunch of other constraints // don't call setNeedsUpdateConstraints b/c that destroys/recreates a bunch of other constraints
// if there is no lastSubviewBottomConstraint, then we already need a constraint update, so we don't need to do anything here // if there is no lastSubviewBottomConstraint, then we already need a constraint update, so we don't need to do anything here
if let lastSubviewBottomConstraint { if let lastSubviewBottomConstraint {
lastSubviewBottomConstraint.isActive = !collapsed lastSubviewBottomConstraint.1.isActive = !collapsed
zeroHeightConstraint.isActive = collapsed zeroHeightConstraint.isActive = collapsed
} }
} }

View File

@ -35,9 +35,11 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
$0.layer.masksToBounds = true $0.layer.masksToBounds = true
$0.layer.cornerCurve = .continuous $0.layer.cornerCurve = .continuous
$0.tintColor = .secondaryLabel $0.tintColor = .secondaryLabel
let heightConstraint = $0.heightAnchor.constraint(equalToConstant: TimelineStatusCollectionViewCell.timelineReasonIconSize)
heightConstraint.identifier = "TimelineReason-Height"
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
// this needs to be lessThanOrEqualTo not just equalTo b/c otherwise intermediate layouts are broken // this needs to be lessThanOrEqualTo not just equalTo b/c otherwise intermediate layouts are broken
$0.heightAnchor.constraint(lessThanOrEqualToConstant: TimelineStatusCollectionViewCell.timelineReasonIconSize), heightConstraint,
$0.widthAnchor.constraint(equalTo: $0.heightAnchor), $0.widthAnchor.constraint(equalTo: $0.heightAnchor),
]) ])
} }
@ -60,16 +62,20 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
$0.addSubview(contentVStack) $0.addSubview(contentVStack)
metaIndicatorsView.translatesAutoresizingMaskIntoConstraints = false metaIndicatorsView.translatesAutoresizingMaskIntoConstraints = false
$0.addSubview(metaIndicatorsView) $0.addSubview(metaIndicatorsView)
let avatarTopConstraint = avatarImageView.topAnchor.constraint(equalTo: $0.topAnchor)
avatarTopConstraint.identifier = "Avatar-Top"
let metaIndicatorsTopConstraint = metaIndicatorsView.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 4)
metaIndicatorsTopConstraint.identifier = "MetaIndicators-Top"
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
avatarImageView.leadingAnchor.constraint(equalTo: $0.leadingAnchor), avatarImageView.leadingAnchor.constraint(equalTo: $0.leadingAnchor),
avatarImageView.topAnchor.constraint(equalTo: $0.topAnchor), avatarTopConstraint,
contentVStack.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 8), contentVStack.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 8),
contentVStack.trailingAnchor.constraint(equalTo: $0.trailingAnchor), contentVStack.trailingAnchor.constraint(equalTo: $0.trailingAnchor),
contentVStack.topAnchor.constraint(equalTo: $0.topAnchor), contentVStack.topAnchor.constraint(equalTo: $0.topAnchor),
contentVStack.bottomAnchor.constraint(equalTo: $0.bottomAnchor), contentVStack.bottomAnchor.constraint(equalTo: $0.bottomAnchor),
metaIndicatorsView.leadingAnchor.constraint(greaterThanOrEqualTo: $0.leadingAnchor), metaIndicatorsView.leadingAnchor.constraint(greaterThanOrEqualTo: $0.leadingAnchor),
metaIndicatorsView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor), metaIndicatorsView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
metaIndicatorsView.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 4), metaIndicatorsTopConstraint,
]) ])
} }
@ -177,6 +183,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
$0.tintAdjustmentMode = .normal $0.tintAdjustmentMode = .normal
$0.setContentHuggingPriority(.defaultHigh, for: .vertical) $0.setContentHuggingPriority(.defaultHigh, for: .vertical)
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside) $0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
$0.setContentCompressionResistancePriority(.required, for: .vertical)
} }
let contentContainer = StatusContentContainer<StatusContentTextView, StatusPollView>(useTopSpacer: false).configure { let contentContainer = StatusContentContainer<StatusContentTextView, StatusPollView>(useTopSpacer: false).configure {
@ -316,17 +323,12 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
} }
mainContainerTopToReblogLabelConstraint = mainContainer.topAnchor.constraint(equalTo: timelineReasonHStack.bottomAnchor, constant: 4) mainContainerTopToReblogLabelConstraint = mainContainer.topAnchor.constraint(equalTo: timelineReasonHStack.bottomAnchor, constant: 4)
mainContainerTopToReblogLabelConstraint.identifier = "MainContainerTopToReblog"
mainContainerTopToSelfConstraint = mainContainer.topAnchor.constraint(equalTo: statusContainer.topAnchor, constant: 8) mainContainerTopToSelfConstraint = mainContainer.topAnchor.constraint(equalTo: statusContainer.topAnchor, constant: 8)
// when flipping between topToReblog and topToSelf constraints, the framework sometimes thinks both of them should be active simultaneously mainContainerTopToSelfConstraint.identifier = "MainContainerTopToSelf"
// even though the code never does that; so let this one get broken temporarily
mainContainerTopToSelfConstraint.priority = .init(999)
mainContainerBottomToActionsConstraint = mainContainer.bottomAnchor.constraint(equalTo: actionsContainer.topAnchor, constant: -4) mainContainerBottomToActionsConstraint = mainContainer.bottomAnchor.constraint(equalTo: actionsContainer.topAnchor, constant: -4)
mainContainerBottomToSelfConstraint = mainContainer.bottomAnchor.constraint(equalTo: statusContainer.bottomAnchor, constant: -6) mainContainerBottomToSelfConstraint = mainContainer.bottomAnchor.constraint(equalTo: statusContainer.bottomAnchor, constant: -6)
let metaIndicatorsBottomConstraint = metaIndicatorsView.bottomAnchor.constraint(lessThanOrEqualTo: statusContainer.bottomAnchor, constant: -6)
// sometimes during intermediate layouts, there are conflicting constraints, so let this one get broken temporarily, to avoid a bunch of printing
metaIndicatorsBottomConstraint.priority = .init(999)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
// why is this 4 but the mainContainerTopSelfConstraint constant 8? because this looks more balanced // why is this 4 but the mainContainerTopSelfConstraint constant 8? because this looks more balanced
timelineReasonHStack.topAnchor.constraint(equalTo: statusContainer.topAnchor, constant: 4), timelineReasonHStack.topAnchor.constraint(equalTo: statusContainer.topAnchor, constant: 4),
@ -335,13 +337,14 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
mainContainer.leadingAnchor.constraint(equalTo: statusContainer.leadingAnchor, constant: 16), mainContainer.leadingAnchor.constraint(equalTo: statusContainer.leadingAnchor, constant: 16),
mainContainer.trailingAnchor.constraint(equalTo: statusContainer.trailingAnchor, constant: -16), mainContainer.trailingAnchor.constraint(equalTo: statusContainer.trailingAnchor, constant: -16),
mainContainerBottomToActionsConstraint,
actionsContainer.leadingAnchor.constraint(equalTo: statusContainer.leadingAnchor, constant: 16), actionsContainer.leadingAnchor.constraint(equalTo: statusContainer.leadingAnchor, constant: 16),
actionsContainer.trailingAnchor.constraint(equalTo: statusContainer.trailingAnchor, constant: -16), actionsContainer.trailingAnchor.constraint(equalTo: statusContainer.trailingAnchor, constant: -16),
// yes, this is deliberately 6. 4 looks to cramped, 8 looks uneven // yes, this is deliberately 6. 4 looks to cramped, 8 looks uneven
actionsContainer.bottomAnchor.constraint(equalTo: statusContainer.bottomAnchor, constant: -6), actionsContainer.bottomAnchor.constraint(equalTo: statusContainer.bottomAnchor, constant: -6),
metaIndicatorsBottomConstraint, metaIndicatorsView.bottomAnchor.constraint(lessThanOrEqualTo: statusContainer.bottomAnchor, constant: -6),
]) ])
updateActionsVisibility() updateActionsVisibility()
@ -590,8 +593,14 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
} }
timelineReasonHStack.isHidden = hideTimelineReason timelineReasonHStack.isHidden = hideTimelineReason
mainContainerTopToReblogLabelConstraint.isActive = !hideTimelineReason // do this to make sure the currently active constraint is deactivated first
mainContainerTopToSelfConstraint.isActive = hideTimelineReason if hideTimelineReason {
mainContainerTopToReblogLabelConstraint.isActive = false
mainContainerTopToSelfConstraint.isActive = true
} else {
mainContainerTopToSelfConstraint.isActive = false
mainContainerTopToReblogLabelConstraint.isActive = true
}
doUpdateUI(status: status, precomputedContent: precomputedContent) doUpdateUI(status: status, precomputedContent: precomputedContent)
@ -690,11 +699,11 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
} }
private func updateActionsVisibility() { private func updateActionsVisibility() {
if Preferences.shared.hideActionsInTimeline { if Preferences.shared.hideActionsInTimeline && !actionsContainer.isHidden {
actionsContainer.isHidden = true actionsContainer.isHidden = true
mainContainerBottomToSelfConstraint.isActive = true
mainContainerBottomToActionsConstraint.isActive = false mainContainerBottomToActionsConstraint.isActive = false
} else { mainContainerBottomToSelfConstraint.isActive = true
} else if !Preferences.shared.hideActionsInTimeline && actionsContainer.isHidden {
actionsContainer.isHidden = false actionsContainer.isHidden = false
mainContainerBottomToSelfConstraint.isActive = false mainContainerBottomToSelfConstraint.isActive = false
mainContainerBottomToActionsConstraint.isActive = true mainContainerBottomToActionsConstraint.isActive = true