// // StatusContentContainer.swift // Tusker // // Created by Shadowfacts on 10/2/22. // Copyright © 2022 Shadowfacts. All rights reserved. // import UIKit protocol StatusContentPollView: UIView { func estimateHeight(effectiveWidth: CGFloat) -> CGFloat } class StatusContentContainer: UIView { private var useTopSpacer = false private let topSpacer = UIView().configure { $0.backgroundColor = .clear // other 4pt is provided by this view's own spacing $0.heightAnchor.constraint(equalToConstant: 4).isActive = true } let contentTextView = ContentView().configure { $0.adjustsFontForContentSizeCategory = true $0.isScrollEnabled = false $0.backgroundColor = nil $0.isEditable = false $0.isSelectable = false } private static var cardViewHeight: CGFloat { 90 } let cardView = StatusCardView().configure { NSLayoutConstraint.activate([ $0.heightAnchor.constraint(equalToConstant: StatusContentContainer.cardViewHeight), ]) } let attachmentsView = AttachmentsContainerView() let pollView = PollView() private var arrangedSubviews: [UIView] { if useTopSpacer { return [topSpacer, contentTextView, cardView, attachmentsView, pollView] } else { return [contentTextView, cardView, attachmentsView, pollView] } } private var isHiddenObservations: [NSKeyValueObservation] = [] private var verticalConstraints: [NSLayoutConstraint] = [] private var lastSubviewBottomConstraint: NSLayoutConstraint? private var zeroHeightConstraint: NSLayoutConstraint! private var isCollapsed = false var visibleSubviewHeight: CGFloat { subviews.filter { !$0.isHidden }.map(\.bounds.height).reduce(0, +) } init(useTopSpacer: Bool) { self.useTopSpacer = useTopSpacer super.init(frame: .zero) for subview in arrangedSubviews { subview.translatesAutoresizingMaskIntoConstraints = false addSubview(subview) NSLayoutConstraint.activate([ subview.leadingAnchor.constraint(equalTo: leadingAnchor), subview.trailingAnchor.constraint(equalTo: trailingAnchor), ]) } // this constraint needs to have low priority so that during the collapse/expand animation, the content container is the view that shrinks/expands zeroHeightConstraint = heightAnchor.constraint(equalToConstant: 0) zeroHeightConstraint.priority = .defaultLow setNeedsUpdateConstraints() isHiddenObservations = arrangedSubviews.map { $0.observe(\.isHidden) { [unowned self] _, _ in self.setNeedsUpdateConstraints() } } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func updateConstraints() { NSLayoutConstraint.deactivate(verticalConstraints) verticalConstraints = [] var lastVisibleSubview: UIView? for subview in arrangedSubviews { guard !subview.isHidden else { continue } if let lastVisibleSubview { verticalConstraints.append(subview.topAnchor.constraint(equalTo: lastVisibleSubview.bottomAnchor, constant: 4)) } else { verticalConstraints.append(subview.topAnchor.constraint(equalTo: topAnchor)) } lastVisibleSubview = subview } 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 super.updateConstraints() } func setCollapsed(_ collapsed: Bool) { guard collapsed != isCollapsed else { return } isCollapsed = collapsed // 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 let lastSubviewBottomConstraint { lastSubviewBottomConstraint.isActive = !collapsed zeroHeightConstraint.isActive = collapsed } } // used only for collapsing automatically based on height, doesn't need to be accurate // just roughly inline with the content height func estimateVisibleSubviewHeight(effectiveWidth: CGFloat) -> CGFloat { var height: CGFloat = 0 height += contentTextView.sizeThatFits(CGSize(width: effectiveWidth, height: UIView.layoutFittingCompressedSize.height)).height if !cardView.isHidden { height += StatusContentContainer.cardViewHeight } if !attachmentsView.isHidden { height += effectiveWidth / attachmentsView.aspectRatio } if !pollView.isHidden { let pollHeight = pollView.estimateHeight(effectiveWidth: effectiveWidth) height += pollHeight } return height } }