Compare commits

..

5 Commits

15 changed files with 201 additions and 69 deletions

View File

@ -223,6 +223,8 @@
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; }; D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; }; D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; }; D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; }; D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; }; D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; };
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; }; D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; };
@ -510,6 +512,8 @@
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; }; D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; }; D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; };
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; }; D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = "<group>"; }; D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = "<group>"; };
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; }; D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; };
@ -1159,6 +1163,7 @@
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */, D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */, D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */, D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
); );
path = Utilities; path = Utilities;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1210,6 +1215,7 @@
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */, D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
D6028B9A2150811100F223B9 /* MastodonCache.swift */, D6028B9A2150811100F223B9 /* MastodonCache.swift */,
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */, D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
D6F1F84E2193B9BE00F5FE67 /* Caching */, D6F1F84E2193B9BE00F5FE67 /* Caching */,
D6757A7A2157E00100721E32 /* XCallbackURL */, D6757A7A2157E00100721E32 /* XCallbackURL */,
D62D241E217AA46B005076CC /* Shortcuts */, D62D241E217AA46B005076CC /* Shortcuts */,
@ -1689,6 +1695,7 @@
D627943523A5525100D38C68 /* StatusActivity.swift in Sources */, D627943523A5525100D38C68 /* StatusActivity.swift in Sources */,
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */, D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */, D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */,
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */, D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */, D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */,
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */, D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */,
@ -1703,6 +1710,7 @@
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */, D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */, D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */, D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */,
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */,
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */, D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */,
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */, D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */, D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,

View File

@ -13,7 +13,7 @@ import AVKit
class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, LargeImageAnimatableViewController { class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, LargeImageAnimatableViewController {
let attachments: [Attachment] let attachments: [Attachment]
let sourcesInfo: [LargeImageViewController.SourceInfo?] let sourceViews: WeakArray<UIImageView>
let startIndex: Int let startIndex: Int
let pages: [UIViewController] let pages: [UIViewController]
@ -26,12 +26,13 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
return index return index
} }
var animationSourceInfo: LargeImageViewController.SourceInfo? { sourcesInfo[currentIndex] } var animationSourceView: UIImageView? { sourceViews[currentIndex] }
var animationImage: UIImage? { var animationImage: UIImage? {
if let sourceImage = sourcesInfo[currentIndex]?.image { if let page = pages[currentIndex] as? LoadingLargeImageViewController,
return sourceImage let image = page.largeImageVC?.image {
return image
} else { } else {
return (pages[currentIndex] as? LoadingLargeImageViewController)?.largeImageVC?.image return animationSourceView?.image
} }
} }
var animationGifData: Data? { var animationGifData: Data? {
@ -59,9 +60,9 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
} }
} }
init(attachments: [Attachment], sourcesInfo: [LargeImageViewController.SourceInfo?], startIndex: Int) { init(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) {
self.attachments = attachments self.attachments = attachments
self.sourcesInfo = sourcesInfo self.sourceViews = WeakArray(sourceViews)
self.startIndex = startIndex self.startIndex = startIndex
self.pages = attachments.map { self.pages = attachments.map {

View File

@ -233,7 +233,7 @@ class ComposeViewController: UIViewController {
return [] return []
} }
return StatusFormat.allCases.map { (format) in var formatButtons = StatusFormat.allCases.map { (format) -> UIBarButtonItem in
let item: UIBarButtonItem let item: UIBarButtonItem
if let image = format.image { if let image = format.image {
item = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(formatButtonPressed(_:))) item = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(formatButtonPressed(_:)))
@ -248,6 +248,14 @@ class ComposeViewController: UIViewController {
item.accessibilityLabel = format.accessibilityLabel item.accessibilityLabel = format.accessibilityLabel
return item return item
} }
for i in (1..<StatusFormat.allCases.count).reversed() {
let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
spacer.width = 8
formatButtons.insert(spacer, at: i)
}
return formatButtons
} }
@objc func adjustForKeyboard(notification: NSNotification) { @objc func adjustForKeyboard(notification: NSNotification) {

View File

@ -11,10 +11,8 @@ import Gifu
class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController { class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController {
typealias SourceInfo = (image: UIImage?, frame: CGRect, cornerRadius: CGFloat) weak var animationSourceView: UIImageView?
var animationImage: UIImage? { image ?? animationSourceView?.image }
var animationSourceInfo: SourceInfo?
var animationImage: UIImage? { animationSourceInfo?.image ?? image }
var animationGifData: Data? { gifData } var animationGifData: Data? { gifData }
var dismissInteractionController: LargeImageInteractionController? var dismissInteractionController: LargeImageInteractionController?
@ -59,10 +57,10 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
return !controlsVisible return !controlsVisible
} }
init(image: UIImage, description: String?, sourceInfo: SourceInfo?) { init(image: UIImage, description: String?, sourceView: UIImageView?) {
self.image = image self.image = image
self.imageDescription = description self.imageDescription = description
self.animationSourceInfo = sourceInfo self.animationSourceView = sourceView
super.init(nibName: "LargeImageViewController", bundle: nil) super.init(nibName: "LargeImageViewController", bundle: nil)
@ -182,6 +180,8 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
return zoomRect return zoomRect
} }
// MARK: Interaction
func animateZoomOut() { func animateZoomOut() {
UIView.animate(withDuration: 0.3, animations: { UIView.animate(withDuration: 0.3, animations: {
self.scrollView.zoomScale = self.scrollView.minimumZoomScale self.scrollView.zoomScale = self.scrollView.minimumZoomScale

View File

@ -35,8 +35,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
var shrinkGestureEnabled = true var shrinkGestureEnabled = true
var animationSourceInfo: LargeImageViewController.SourceInfo? weak var animationSourceView: UIImageView?
var animationImage: UIImage? { animationSourceInfo?.image ?? largeImageVC?.image } var animationImage: UIImage? { largeImageVC?.image ?? animationSourceView?.image }
var animationGifData: Data? { largeImageVC?.gifData } var animationGifData: Data? { largeImageVC?.gifData }
var dismissInteractionController: LargeImageInteractionController? var dismissInteractionController: LargeImageInteractionController?
@ -108,7 +108,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
func createLargeImage(data: Data) { func createLargeImage(data: Data) {
guard let image = UIImage(data: data) else { return } guard let image = UIImage(data: data) else { return }
largeImageVC = LargeImageViewController(image: image, description: imageDescription, sourceInfo: nil) largeImageVC = LargeImageViewController(image: image, description: imageDescription, sourceView: animationSourceView)
largeImageVC!.initialControlsVisible = initialControlsVisible largeImageVC!.initialControlsVisible = initialControlsVisible
largeImageVC!.shrinkGestureEnabled = false largeImageVC!.shrinkGestureEnabled = false
if url.pathExtension == "gif" { if url.pathExtension == "gif" {

View File

@ -10,12 +10,30 @@ import UIKit
import Gifu import Gifu
protocol LargeImageAnimatableViewController: UIViewController { protocol LargeImageAnimatableViewController: UIViewController {
var animationSourceInfo: LargeImageViewController.SourceInfo? { get } var animationSourceView: UIImageView? { get }
var animationImage: UIImage? { get } var animationImage: UIImage? { get }
var animationGifData: Data? { get } var animationGifData: Data? { get }
var dismissInteractionController: LargeImageInteractionController? { get } var dismissInteractionController: LargeImageInteractionController? { get }
} }
extension LargeImageAnimatableViewController {
func sourceViewFrame(in coordinateSpace: UIView) -> CGRect? {
guard let sourceView = animationSourceView else { return nil }
var sourceFrame = sourceView.convert(sourceView.bounds, to: coordinateSpace)
if let scrollView = coordinateSpace as? UIScrollView {
let scale = scrollView.zoomScale
let width = sourceFrame.width * scale
let height = sourceFrame.height * scale
let x = sourceFrame.minX * scale - scrollView.contentOffset.x + scrollView.frame.minX
let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY
sourceFrame = CGRect(x: x, y: y, width: width, height: height)
}
return sourceFrame
}
}
class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning { class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
@ -32,25 +50,35 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
containerView.addSubview(toVC.view) containerView.addSubview(toVC.view)
let finalVCFrame = transitionContext.finalFrame(for: toVC) let finalVCFrame = transitionContext.finalFrame(for: toVC)
guard let sourceInfo = toVC.animationSourceInfo, guard let sourceView = toVC.animationSourceView,
let sourceFrame = toVC.sourceViewFrame(in: fromVC.view),
let image = toVC.animationImage else { let image = toVC.animationImage else {
toVC.view.frame = finalVCFrame toVC.view.frame = finalVCFrame
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return return
} }
let ratio = image.size.width / image.size.height
let width = finalVCFrame.width
let height = width / ratio
let finalFrame = CGRect(x: finalVCFrame.midX - width / 2, y: finalVCFrame.midY - height / 2, width: width, height: height)
let imageView = GIFImageView(frame: sourceInfo.frame) // use alpha, becaus isHidden makes stack views re-layout
sourceView.alpha = 0
var finalFrameSize = finalVCFrame.inset(by: fromVC.view.safeAreaInsets).size
let newWidth = finalFrameSize.width / image.size.width
let newHeight = finalFrameSize.height / image.size.height
if newHeight < newWidth {
finalFrameSize.width = newHeight * image.size.width
} else {
finalFrameSize.height = newWidth * image.size.height
}
let finalFrame = CGRect(origin: CGPoint(x: finalVCFrame.midX - finalFrameSize.width / 2, y: finalVCFrame.midY - finalFrameSize.height / 2), size: finalFrameSize)
let imageView = GIFImageView(frame: sourceFrame)
imageView.image = image imageView.image = image
if let gifData = toVC.animationGifData { if let gifData = toVC.animationGifData {
imageView.animate(withGIFData: gifData) imageView.animate(withGIFData: gifData)
} }
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = sourceInfo.cornerRadius imageView.layer.cornerRadius = sourceView.layer.cornerRadius
imageView.layer.maskedCorners = sourceView.layer.maskedCorners
imageView.layer.masksToBounds = true imageView.layer.masksToBounds = true
let blackView = UIView(frame: finalVCFrame) let blackView = UIView(frame: finalVCFrame)
@ -79,6 +107,9 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
fromVC.view.isHidden = false fromVC.view.isHidden = false
blackView.removeFromSuperview() blackView.removeFromSuperview()
imageView.removeFromSuperview() imageView.removeFromSuperview()
sourceView.alpha = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}) })
} }

View File

@ -19,7 +19,11 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
init(viewController: UIViewController) { init(viewController: UIViewController) {
super.init() super.init()
self.viewController = viewController self.viewController = viewController
viewController.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))) let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
if #available(iOS 13.4, *) {
panRecognizer.allowedScrollTypesMask = .all
}
viewController.view.addGestureRecognizer(panRecognizer)
} }
@objc func handleGesture(_ recognizer: UIPanGestureRecognizer) { @objc func handleGesture(_ recognizer: UIPanGestureRecognizer) {

View File

@ -27,20 +27,29 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
return return
} }
guard let sourceInfo = fromVC.animationSourceInfo, guard let sourceView = fromVC.animationSourceView,
let sourceFrame = fromVC.sourceViewFrame(in: toVC.view),
let image = fromVC.animationImage else { let image = fromVC.animationImage else {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return return
} }
let originalVCFrame = fromVC.view.frame // use alpha, becaus isHidden makes stack views re-layout
sourceView.alpha = 0
let containerView = transitionContext.containerView let containerView = transitionContext.containerView
let ratio = image.size.width / image.size.height
let width = originalVCFrame.width
let height = width / ratio
let originalFrame = CGRect(x: originalVCFrame.midX - width / 2, y: originalVCFrame.midY - height / 2, width: width, height: height)
let originalVCFrame = fromVC.view.frame
var originalFrameSize = originalVCFrame.inset(by: fromVC.view.safeAreaInsets).size
let newWidth = originalFrameSize.width / image.size.width
let newHeight = originalFrameSize.height / image.size.height
if newHeight < newWidth {
originalFrameSize.width = newHeight * image.size.width
} else {
originalFrameSize.height = newWidth * image.size.height
}
let originalFrame = CGRect(origin: CGPoint(x: originalVCFrame.midX - originalFrameSize.width / 2, y: originalVCFrame.midY - originalFrameSize.height / 2), size: originalFrameSize)
let imageView = GIFImageView(frame: originalFrame) let imageView = GIFImageView(frame: originalFrame)
imageView.image = image imageView.image = image
if let gifData = fromVC.animationGifData { if let gifData = fromVC.animationGifData {
@ -60,8 +69,9 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
let duration = transitionDuration(using: transitionContext) let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, animations: { UIView.animate(withDuration: duration, animations: {
imageView.frame = sourceInfo.frame imageView.frame = sourceFrame
imageView.layer.cornerRadius = sourceInfo.cornerRadius imageView.layer.cornerRadius = sourceView.layer.cornerRadius
imageView.layer.maskedCorners = sourceView.layer.maskedCorners
blackView.alpha = 0 blackView.alpha = 0
}, completion: { _ in }, completion: { _ in
blackView.removeFromSuperview() blackView.removeFromSuperview()
@ -69,6 +79,9 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
if transitionContext.transitionWasCancelled { if transitionContext.transitionWasCancelled {
toVC.view.removeFromSuperview() toVC.view.removeFromSuperview()
} }
sourceView.alpha = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}) })
} }

View File

@ -35,6 +35,12 @@ class InteractivePushTransition: UIPercentDrivenInteractiveTransition {
interactivePushGestureRecognizer.edges = .right interactivePushGestureRecognizer.edges = .right
interactivePushGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!) interactivePushGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
navigationController.view.addGestureRecognizer(interactivePushGestureRecognizer) navigationController.view.addGestureRecognizer(interactivePushGestureRecognizer)
if #available(iOS 13.4, *) {
let trackpadGestureRecognizer = TrackpadScrollGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:)))
trackpadGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
navigationController.view.addGestureRecognizer(trackpadGestureRecognizer)
}
} }
@objc func handleSwipeForward(_ recognizer: UIPanGestureRecognizer) { @objc func handleSwipeForward(_ recognizer: UIPanGestureRecognizer) {

View File

@ -0,0 +1,24 @@
//
// TrackpadScreenEdgePanScrollGestureRecognizer.swift
// Tusker
//
// Created by Shadowfacts on 3/25/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
@available(iOS 13.4, *)
class TrackpadScrollGestureRecognizer: UIPanGestureRecognizer {
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
self.allowedScrollTypesMask = .all
}
override func shouldReceive(_ event: UIEvent) -> Bool {
return event.type == .scroll
}
}

View File

@ -150,30 +150,15 @@ extension TuskerNavigationDelegate where Self: UIViewController {
vc.presentationController?.delegate = compose vc.presentationController?.delegate = compose
present(vc, animated: true) present(vc, animated: true)
} }
private func sourceViewInfo(_ sourceView: UIImageView?) -> LargeImageViewController.SourceInfo? {
guard let sourceView = sourceView else { return nil }
var sourceFrame = sourceView.convert(sourceView.bounds, to: view)
if let scrollView = view as? UIScrollView {
let scale = scrollView.zoomScale
let width = sourceFrame.width * scale
let height = sourceFrame.height * scale
let x = sourceFrame.minX * scale - scrollView.contentOffset.x + scrollView.frame.minX
let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY
sourceFrame = CGRect(x: x, y: y, width: width, height: height)
}
return (image: sourceView.image, frame: sourceFrame, cornerRadius: sourceView.layer.cornerRadius)
}
func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController { func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController {
let vc = LargeImageViewController(image: image, description: description, sourceInfo: sourceViewInfo(sourceView)) let vc = LargeImageViewController(image: image, description: description, sourceView: sourceView)
vc.transitioningDelegate = self vc.transitioningDelegate = self
return vc return vc
} }
func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController { func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController {
let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceInfo: sourceViewInfo(sourceView)) let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceView: sourceView)
vc.transitioningDelegate = self vc.transitioningDelegate = self
vc.gifData = gifData vc.gifData = gifData
return vc return vc
@ -189,7 +174,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController { func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController {
let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description) let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description)
vc.animationSourceInfo = sourceViewInfo(sourceView) vc.animationSourceView = sourceView
vc.transitioningDelegate = self vc.transitioningDelegate = self
return vc return vc
} }
@ -199,8 +184,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
} }
func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController { func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController {
let sourcesInfo = sourceViews.map(sourceViewInfo) let vc = GalleryViewController(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex)
let vc = GalleryViewController(attachments: attachments, sourcesInfo: sourcesInfo, startIndex: startIndex)
vc.transitioningDelegate = self vc.transitioningDelegate = self
return vc return vc
} }

View File

@ -52,6 +52,10 @@ class ProfileHeaderTableViewCell: UITableViewCell {
maskLayer.path = CGPath(ellipseIn: moreButtonVisualEffectView.bounds, transform: nil) maskLayer.path = CGPath(ellipseIn: moreButtonVisualEffectView.bounds, transform: nil)
moreButtonVisualEffectView.layer.mask = maskLayer moreButtonVisualEffectView.layer.mask = maskLayer
if #available(iOS 13.4, *) {
moreButtonVisualEffectView.addInteraction(UIPointerInteraction(delegate: self))
}
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
} }
@ -159,3 +163,12 @@ class ProfileHeaderTableViewCell: UITableViewCell {
} }
} }
extension ProfileHeaderTableViewCell: UIPointerInteractionDelegate {
@available(iOS 13.4, *)
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
let preview = UITargetedPreview(view: moreButtonVisualEffectView)
let rect = CGRect(x: moreButtonVisualEffectView.frame.minX - 4, y: moreButtonVisualEffectView.frame.minY - 4, width: moreButtonVisualEffectView.frame.width + 8, height: moreButtonVisualEffectView.frame.height + 8)
return UIPointerStyle(effect: .highlight(preview), shape: .roundedRect(rect, radius: 4))
}
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16086"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
@ -144,7 +144,7 @@
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="3Bg-XP-d13"> <stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="3Bg-XP-d13">
<rect key="frame" x="0.0" y="257" width="343" height="18"/> <rect key="frame" x="0.0" y="257" width="343" height="18"/>
<subviews> <subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2cc-lE-AdG"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2cc-lE-AdG">
<rect key="frame" x="0.0" y="0.0" width="21" height="18"/> <rect key="frame" x="0.0" y="0.0" width="21" height="18"/>
<accessibility key="accessibilityConfiguration" label="Reply"/> <accessibility key="accessibilityConfiguration" label="Reply"/>
<state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/> <state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/>
@ -152,7 +152,7 @@
<action selector="replyPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="RxZ-zv-lkN"/> <action selector="replyPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="RxZ-zv-lkN"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="DhN-rJ-jdA"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DhN-rJ-jdA">
<rect key="frame" x="107" y="0.0" width="22" height="18"/> <rect key="frame" x="107" y="0.0" width="22" height="18"/>
<accessibility key="accessibilityConfiguration" label="Favorite"/> <accessibility key="accessibilityConfiguration" label="Favorite"/>
<state key="normal" image="star.fill" catalog="system"/> <state key="normal" image="star.fill" catalog="system"/>
@ -160,7 +160,7 @@
<action selector="favoritePressed" destination="iN0-l3-epB" eventType="touchUpInside" id="NCA-iR-VMt"/> <action selector="favoritePressed" destination="iN0-l3-epB" eventType="touchUpInside" id="NCA-iR-VMt"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GUG-f7-Hdy"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="GUG-f7-Hdy">
<rect key="frame" x="215.5" y="0.0" width="22.5" height="18"/> <rect key="frame" x="215.5" y="0.0" width="22.5" height="18"/>
<accessibility key="accessibilityConfiguration" label="Reblog"/> <accessibility key="accessibilityConfiguration" label="Reblog"/>
<state key="normal" image="repeat" catalog="system"/> <state key="normal" image="repeat" catalog="system"/>
@ -168,7 +168,7 @@
<action selector="reblogPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="iIu-Vv-U0I"/> <action selector="reblogPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="iIu-Vv-U0I"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ujo-Ap-dmK"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ujo-Ap-dmK">
<rect key="frame" x="324" y="0.0" width="19" height="18"/> <rect key="frame" x="324" y="0.0" width="19" height="18"/>
<accessibility key="accessibilityConfiguration" label="More Actions"/> <accessibility key="accessibilityConfiguration" label="More Actions"/>
<state key="normal" image="ellipsis" catalog="system"/> <state key="normal" image="ellipsis" catalog="system"/>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16086"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
@ -135,7 +135,7 @@
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" distribution="equalSpacing" alignment="bottom" translatesAutoresizingMaskIntoConstraints="NO" id="Zlb-yt-NTw"> <stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" distribution="equalSpacing" alignment="bottom" translatesAutoresizingMaskIntoConstraints="NO" id="Zlb-yt-NTw">
<rect key="frame" x="0.0" y="202" width="343" height="22"/> <rect key="frame" x="0.0" y="202" width="343" height="22"/>
<subviews> <subviews>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rKF-yF-KIa"> <button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="rKF-yF-KIa">
<rect key="frame" x="0.0" y="0.0" width="21" height="22"/> <rect key="frame" x="0.0" y="0.0" width="21" height="22"/>
<accessibility key="accessibilityConfiguration" label="Reply"/> <accessibility key="accessibilityConfiguration" label="Reply"/>
<state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/> <state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/>
@ -143,7 +143,7 @@
<action selector="replyPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="ybz-3W-jAa"/> <action selector="replyPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="ybz-3W-jAa"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="x0t-TR-jJ4"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="x0t-TR-jJ4">
<rect key="frame" x="107" y="0.0" width="22" height="22"/> <rect key="frame" x="107" y="0.0" width="22" height="22"/>
<accessibility key="accessibilityConfiguration" label="Favorite"/> <accessibility key="accessibilityConfiguration" label="Favorite"/>
<state key="normal" image="star.fill" catalog="system"/> <state key="normal" image="star.fill" catalog="system"/>
@ -151,7 +151,7 @@
<action selector="favoritePressed" destination="iN0-l3-epB" eventType="touchUpInside" id="8Q8-Rz-k02"/> <action selector="favoritePressed" destination="iN0-l3-epB" eventType="touchUpInside" id="8Q8-Rz-k02"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6tW-z8-Qh9"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6tW-z8-Qh9">
<rect key="frame" x="215.5" y="0.0" width="22.5" height="22"/> <rect key="frame" x="215.5" y="0.0" width="22.5" height="22"/>
<accessibility key="accessibilityConfiguration" label="Reblog"/> <accessibility key="accessibilityConfiguration" label="Reblog"/>
<state key="normal" image="repeat" catalog="system"/> <state key="normal" image="repeat" catalog="system"/>
@ -159,7 +159,7 @@
<action selector="reblogPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="Wa2-ZA-TBo"/> <action selector="reblogPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="Wa2-ZA-TBo"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="982-J4-NGl"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="982-J4-NGl">
<rect key="frame" x="324" y="0.0" width="19" height="22"/> <rect key="frame" x="324" y="0.0" width="19" height="22"/>
<accessibility key="accessibilityConfiguration" label="More Actions"/> <accessibility key="accessibilityConfiguration" label="More Actions"/>
<state key="normal" image="ellipsis" catalog="system"/> <state key="normal" image="ellipsis" catalog="system"/>

40
Tusker/WeakArray.swift Normal file
View File

@ -0,0 +1,40 @@
//
// WeakArray.swift
// Tusker
//
// Created by Shadowfacts on 3/25/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import Foundation
fileprivate class WeakWrapper<T: AnyObject> {
weak var value: T?
init(_ value: T?) {
self.value = value
}
}
struct WeakArray<Element: AnyObject>: Collection {
private var array: [WeakWrapper<Element>]
var startIndex: Int { array.startIndex }
var endIndex: Int { array.endIndex }
init(_ elements: [Element]) {
array = elements.map { WeakWrapper($0) }
}
init(_ elements: [Element?]) {
array = elements.map { WeakWrapper($0) }
}
subscript(_ index: Int) -> Element? {
return array[index].value
}
func index(after i: Int) -> Int {
return array.index(after: i)
}
}