Compare commits
No commits in common. "47b838a3867325344779b7a47f2bb4214c50584a" and "0a8d50cc27dc91c321f7962d7ab3a19babe607df" have entirely different histories.
47b838a386
...
0a8d50cc27
|
@ -17,16 +17,17 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
|
||||
@IBOutlet weak var scrollView: UIScrollView!
|
||||
@IBOutlet weak var topControlsView: UIView!
|
||||
@IBOutlet weak var topControlsHeightConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var shareButton: UIButton!
|
||||
@IBOutlet weak var shareButtonTopConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var shareButtonLeadingConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var closeButton: UIButton!
|
||||
@IBOutlet weak var closeButtonTopConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var closeButtonTrailingConstraint: NSLayoutConstraint!
|
||||
|
||||
@IBOutlet weak var bottomControlsView: UIView!
|
||||
@IBOutlet weak var descriptionLabel: UILabel!
|
||||
|
||||
private var shareContainer: UIView!
|
||||
private var shareImage: UIImageView!
|
||||
private var shareButtonTopConstraint: NSLayoutConstraint!
|
||||
private var shareButtonLeadingConstraint: NSLayoutConstraint!
|
||||
private var closeButtonTopConstraint: NSLayoutConstraint!
|
||||
private var closeButtonTrailingConstraint: NSLayoutConstraint!
|
||||
|
||||
var contentView: LargeImageContentView {
|
||||
didSet {
|
||||
oldValue.removeFromSuperview()
|
||||
|
@ -85,13 +86,9 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
super.viewDidLoad()
|
||||
|
||||
setupContentView()
|
||||
setupControls()
|
||||
|
||||
setControlsVisible(initialControlsVisible, animated: false)
|
||||
if contentView.activityItemsForSharing.isEmpty {
|
||||
shareContainer.isUserInteractionEnabled = false
|
||||
shareImage.tintColor = .systemGray
|
||||
}
|
||||
shareButton.isEnabled = !contentView.activityItemsForSharing.isEmpty
|
||||
|
||||
scrollView.delegate = self
|
||||
|
||||
|
@ -129,62 +126,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
])
|
||||
}
|
||||
|
||||
private func setupControls() {
|
||||
shareContainer = UIView()
|
||||
shareContainer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(sharePressed)))
|
||||
shareContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
topControlsView.addSubview(shareContainer)
|
||||
shareImage = UIImageView(image: UIImage(systemName: "square.and.arrow.up"))
|
||||
shareImage.tintColor = .white
|
||||
shareImage.contentMode = .scaleAspectFit
|
||||
shareImage.translatesAutoresizingMaskIntoConstraints = false
|
||||
shareContainer.addSubview(shareImage)
|
||||
shareButtonTopConstraint = shareImage.topAnchor.constraint(greaterThanOrEqualTo: shareContainer.topAnchor)
|
||||
shareButtonLeadingConstraint = shareImage.leadingAnchor.constraint(greaterThanOrEqualTo: shareContainer.leadingAnchor)
|
||||
NSLayoutConstraint.activate([
|
||||
shareContainer.topAnchor.constraint(equalTo: topControlsView.topAnchor),
|
||||
shareContainer.leadingAnchor.constraint(equalTo: topControlsView.leadingAnchor),
|
||||
shareContainer.bottomAnchor.constraint(equalTo: topControlsView.bottomAnchor),
|
||||
shareContainer.widthAnchor.constraint(greaterThanOrEqualToConstant: 50),
|
||||
shareContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 50),
|
||||
|
||||
shareImage.centerXAnchor.constraint(equalTo: shareContainer.centerXAnchor),
|
||||
shareImage.centerYAnchor.constraint(equalTo: shareContainer.centerYAnchor),
|
||||
shareButtonTopConstraint,
|
||||
shareButtonLeadingConstraint,
|
||||
|
||||
shareImage.widthAnchor.constraint(equalToConstant: 24),
|
||||
shareImage.heightAnchor.constraint(equalToConstant: 24),
|
||||
])
|
||||
|
||||
let closeContainer = UIView()
|
||||
closeContainer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(closeButtonPressed)))
|
||||
closeContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
topControlsView.addSubview(closeContainer)
|
||||
let closeImage = UIImageView(image: UIImage(systemName: "xmark"))
|
||||
closeImage.tintColor = .white
|
||||
closeImage.contentMode = .scaleAspectFit
|
||||
closeImage.translatesAutoresizingMaskIntoConstraints = false
|
||||
closeContainer.addSubview(closeImage)
|
||||
closeButtonTopConstraint = closeImage.topAnchor.constraint(greaterThanOrEqualTo: closeContainer.topAnchor)
|
||||
closeButtonTrailingConstraint = closeContainer.trailingAnchor.constraint(greaterThanOrEqualTo: closeImage.trailingAnchor)
|
||||
NSLayoutConstraint.activate([
|
||||
closeContainer.topAnchor.constraint(equalTo: topControlsView.topAnchor),
|
||||
closeContainer.trailingAnchor.constraint(equalTo: topControlsView.trailingAnchor),
|
||||
closeContainer.bottomAnchor.constraint(equalTo: closeContainer.bottomAnchor),
|
||||
closeContainer.widthAnchor.constraint(greaterThanOrEqualToConstant: 50),
|
||||
closeContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 50),
|
||||
|
||||
closeImage.centerXAnchor.constraint(equalTo: closeContainer.centerXAnchor),
|
||||
closeImage.centerYAnchor.constraint(equalTo: closeContainer.centerYAnchor),
|
||||
closeButtonTopConstraint,
|
||||
closeButtonTrailingConstraint,
|
||||
|
||||
closeImage.widthAnchor.constraint(equalToConstant: 24),
|
||||
closeImage.heightAnchor.constraint(equalToConstant: 24),
|
||||
])
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
|
@ -215,7 +156,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
// since the corner radius didn't change
|
||||
let notchWidth: CGFloat = 210
|
||||
let earWidth = (view.bounds.width - notchWidth) / 2
|
||||
let offset = (earWidth - shareImage.bounds.width) / 2
|
||||
let offset = (earWidth - shareButton.bounds.width) / 2
|
||||
shareButtonLeadingConstraint.constant = offset
|
||||
closeButtonTrailingConstraint.constant = offset
|
||||
} else if pillDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
||||
|
@ -334,7 +275,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
|
||||
@IBAction func sharePressed(_ sender: Any) {
|
||||
let activityVC = UIActivityViewController(activityItems: contentView.activityItemsForSharing, applicationActivities: nil)
|
||||
activityVC.popoverPresentationController?.sourceView = shareImage
|
||||
activityVC.popoverPresentationController?.sourceView = shareButton
|
||||
present(activityVC, animated: true)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,8 +11,15 @@
|
|||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="LargeImageViewController" customModule="Tusker" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="bottomControlsView" destination="rPa-Zu-T6g" id="Rgz-AQ-9nt"/>
|
||||
<outlet property="closeButton" destination="pnA-ne-k0v" id="RPP-cB-9ap"/>
|
||||
<outlet property="closeButtonTopConstraint" destination="ImD-2H-0XK" id="DUe-b1-a2N"/>
|
||||
<outlet property="closeButtonTrailingConstraint" destination="JFe-ig-3Ic" id="cWO-Rr-y3F"/>
|
||||
<outlet property="descriptionLabel" destination="eo5-fc-RV8" id="vrW-RJ-y5k"/>
|
||||
<outlet property="scrollView" destination="Skj-xq-AgQ" id="TFb-zF-m1b"/>
|
||||
<outlet property="shareButton" destination="vhp-0u-Q0S" id="JZS-K9-4w9"/>
|
||||
<outlet property="shareButtonLeadingConstraint" destination="MJx-2r-p0k" id="Dn5-Eg-Pid"/>
|
||||
<outlet property="shareButtonTopConstraint" destination="sgG-dC-xXP" id="Rjp-od-00F"/>
|
||||
<outlet property="topControlsHeightConstraint" destination="6XT-D6-8FS" id="mTB-LF-50H"/>
|
||||
<outlet property="topControlsView" destination="kHo-B9-R7a" id="8sJ-xQ-7ix"/>
|
||||
<outlet property="view" destination="BJw-5C-9nT" id="1C2-VA-mNf"/>
|
||||
</connections>
|
||||
|
@ -26,8 +33,45 @@
|
|||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<gestureRecognizers/>
|
||||
</scrollView>
|
||||
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kHo-B9-R7a">
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kHo-B9-R7a">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="36"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vhp-0u-Q0S">
|
||||
<rect key="frame" x="16" y="16" width="20" height="20"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="20" id="4tF-oL-qXT"/>
|
||||
<constraint firstAttribute="width" constant="20" id="zWx-jJ-dBj"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<state key="normal" image="square.and.arrow.up" catalog="system"/>
|
||||
<connections>
|
||||
<action selector="sharePressed:" destination="-1" eventType="touchUpInside" id="7Oz-zv-m2t"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pnA-ne-k0v">
|
||||
<rect key="frame" x="339" y="16" width="20" height="20"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="20" id="eg0-hN-rda"/>
|
||||
<constraint firstAttribute="height" constant="20" id="fmA-pI-8WB"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<state key="normal" image="xmark" catalog="system"/>
|
||||
<connections>
|
||||
<action selector="closeButtonPressed:" destination="-1" eventType="touchUpInside" id="7o3-ET-EMo"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" secondItem="pnA-ne-k0v" secondAttribute="height" constant="16" id="6XT-D6-8FS"/>
|
||||
<constraint firstItem="pnA-ne-k0v" firstAttribute="top" secondItem="kHo-B9-R7a" secondAttribute="top" constant="16" id="ImD-2H-0XK"/>
|
||||
<constraint firstAttribute="trailing" secondItem="pnA-ne-k0v" secondAttribute="trailing" constant="16" id="JFe-ig-3Ic"/>
|
||||
<constraint firstItem="vhp-0u-Q0S" firstAttribute="leading" secondItem="kHo-B9-R7a" secondAttribute="leading" constant="16" id="MJx-2r-p0k"/>
|
||||
<constraint firstAttribute="bottom" secondItem="vhp-0u-Q0S" secondAttribute="bottom" id="fi6-JS-UmZ"/>
|
||||
<constraint firstAttribute="bottom" secondItem="pnA-ne-k0v" secondAttribute="bottom" id="hEU-VY-WTd"/>
|
||||
<constraint firstItem="vhp-0u-Q0S" firstAttribute="top" secondItem="kHo-B9-R7a" secondAttribute="top" constant="16" id="sgG-dC-xXP"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rPa-Zu-T6g">
|
||||
<rect key="frame" x="0.0" y="622.5" width="375" height="44.5"/>
|
||||
|
@ -66,4 +110,8 @@
|
|||
<point key="canvasLocation" x="-164" y="476"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="square.and.arrow.up" catalog="system" width="115" height="128"/>
|
||||
<image name="xmark" catalog="system" width="128" height="113"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
|
@ -246,9 +246,17 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
snapshot.insertItems(presentItems.map { .status(id: $0, state: .unknown) }, beforeItem: .gap)
|
||||
|
||||
if applySnapshotBeforeScrolling {
|
||||
let firstVisibleIndexPath = collectionView.indexPathsForVisibleItems.min()!
|
||||
let firstVisibleItem = dataSource.itemIdentifier(for: firstVisibleIndexPath)!
|
||||
applySnapshot(snapshot, maintainingBottomRelativeScrollPositionOf: firstVisibleItem)
|
||||
// use a snapshot of the collection view to hide the flicker as the content offset changes and then changes back
|
||||
let snapshotView = collectionView.snapshotView(afterScreenUpdates: false)!
|
||||
snapshotView.layer.zPosition = 1000
|
||||
snapshotView.frame = view.bounds
|
||||
view.addSubview(snapshotView)
|
||||
|
||||
let bottomOffset = collectionView.contentSize.height - collectionView.contentOffset.y
|
||||
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
||||
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentSize.height - bottomOffset)
|
||||
snapshotView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
var config = ToastConfiguration(title: "Jump to present")
|
||||
|
@ -268,50 +276,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
}
|
||||
}
|
||||
|
||||
// NOTE: this only works when items are being inserted ABOVE the item to maintain
|
||||
private func applySnapshot(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, maintainingBottomRelativeScrollPositionOf itemToMaintain: Item) {
|
||||
// use a snapshot of the collection view to hide the flicker as the content offset changes and then changes back
|
||||
let snapshotView = collectionView.snapshotView(afterScreenUpdates: false)!
|
||||
snapshotView.layer.zPosition = 1000
|
||||
snapshotView.frame = view.bounds
|
||||
view.addSubview(snapshotView)
|
||||
|
||||
var firstItemAfterOriginalGapOffsetFromTop: CGFloat = 0
|
||||
if let indexPath = dataSource.indexPath(for: itemToMaintain),
|
||||
let cell = collectionView.cellForItem(at: indexPath) {
|
||||
// subtract top safe area inset b/c scrollToItem at .top aligns the top of the cell to the top of the safe area
|
||||
firstItemAfterOriginalGapOffsetFromTop = cell.convert(.zero, to: view).y - view.safeAreaInsets.top
|
||||
}
|
||||
|
||||
dataSource.apply(snapshot, animatingDifferences: false) {
|
||||
if let indexPathOfItemAfterOriginalGap = self.dataSource.indexPath(for: itemToMaintain) {
|
||||
// scroll up until we've accumulated enough MEASURED height that we can put the
|
||||
// firstItemAfterOriginalGapCell at the top of the screen and then scroll down by
|
||||
// firstItemAfterOriginalGapOffsetFromTop without intruding into unmeasured area
|
||||
var cur = indexPathOfItemAfterOriginalGap
|
||||
var amountScrolledUp: CGFloat = 0
|
||||
while true {
|
||||
if cur.row <= 0 {
|
||||
break
|
||||
}
|
||||
if let cell = self.collectionView.cellForItem(at: indexPathOfItemAfterOriginalGap),
|
||||
cell.convert(.zero, to: self.view).y - self.view.safeAreaInsets.top > firstItemAfterOriginalGapOffsetFromTop {
|
||||
break
|
||||
}
|
||||
cur = IndexPath(row: cur.row - 1, section: cur.section)
|
||||
self.collectionView.scrollToItem(at: cur, at: .top, animated: false)
|
||||
self.collectionView.layoutIfNeeded()
|
||||
let attrs = self.collectionView.layoutAttributesForItem(at: cur)!
|
||||
amountScrolledUp += attrs.size.height
|
||||
}
|
||||
self.collectionView.contentOffset.y += amountScrolledUp
|
||||
self.collectionView.contentOffset.y -= firstItemAfterOriginalGapOffsetFromTop
|
||||
}
|
||||
|
||||
snapshotView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TimelineViewController {
|
||||
|
@ -575,8 +539,29 @@ extension TimelineViewController {
|
|||
}
|
||||
|
||||
if addedItems {
|
||||
let firstItemAfterOriginalGap = statusItems[gapIndex + 1]
|
||||
applySnapshot(snapshot, maintainingBottomRelativeScrollPositionOf: firstItemAfterOriginalGap)
|
||||
// use a snapshot of the collection view to hide the flicker as the content offset changes and then changes back
|
||||
let snapshotView = collectionView.snapshotView(afterScreenUpdates: false)!
|
||||
snapshotView.layer.zPosition = 1000
|
||||
snapshotView.frame = view.bounds
|
||||
view.addSubview(snapshotView)
|
||||
|
||||
// Yes, these are all load bearing. Setting the contentOffset seems to cause the collection view to recalculate the size
|
||||
// of some cells, thus changing the contentSize and the offset necessary to match to match the bottom offset.
|
||||
// Three DispatchQueue.main.async's seems to be the fewest we can reliably get away with.
|
||||
let bottomOffset = collectionView.contentSize.height - collectionView.contentOffset.y
|
||||
dataSource.apply(snapshot, animatingDifferences: false) {
|
||||
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentSize.height - bottomOffset)
|
||||
DispatchQueue.main.async {
|
||||
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentSize.height - bottomOffset)
|
||||
DispatchQueue.main.async {
|
||||
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentSize.height - bottomOffset)
|
||||
DispatchQueue.main.async {
|
||||
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentSize.height - bottomOffset)
|
||||
snapshotView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dataSource.apply(snapshot, animatingDifferences: true) {}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue