Fix multi-attachment layout issues

This commit is contained in:
Shadowfacts 2019-06-16 22:39:46 -04:00
parent 55fc032f36
commit 85ed53b990
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
10 changed files with 124 additions and 56 deletions

View File

@ -62,7 +62,7 @@ class AttachmentViewController: UIViewController {
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: attachment.description, sourceFrame: nil, sourceCornerRadius: nil) largeImageVC = LargeImageViewController(image: image, description: attachment.description, sourceInfo: nil)
largeImageVC!.initialControlsVisible = initialControlsVisible largeImageVC!.initialControlsVisible = initialControlsVisible
largeImageVC!.shrinkGestureEnabled = false largeImageVC!.shrinkGestureEnabled = false
if attachment.url.pathExtension == "gif" { if attachment.url.pathExtension == "gif" {

View File

@ -13,7 +13,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
var dismissInteractionController: LargeImageInteractionController? var dismissInteractionController: LargeImageInteractionController?
let attachments: [Attachment] let attachments: [Attachment]
let sourcesInfo: [(CGRect, CGFloat)] let sourcesInfo: [LargeImageViewController.SourceInfo?]
let startIndex: Int let startIndex: Int
let pages: [AttachmentViewController] let pages: [AttachmentViewController]
@ -34,7 +34,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
viewControllers?.first viewControllers?.first
} }
init(attachments: [Attachment], sourcesInfo: [(CGRect, CGFloat)], startIndex: Int) { init(attachments: [Attachment], sourcesInfo: [LargeImageViewController.SourceInfo?], startIndex: Int) {
self.attachments = attachments self.attachments = attachments
self.sourcesInfo = sourcesInfo self.sourcesInfo = sourcesInfo
self.startIndex = startIndex self.startIndex = startIndex

View File

@ -19,10 +19,15 @@ class GalleryExpandAnimationController: NSObject, UIViewControllerAnimatedTransi
let toVC = transitionContext.viewController(forKey: .to) as? GalleryViewController else { let toVC = transitionContext.viewController(forKey: .to) as? GalleryViewController else {
return return
} }
let attachment = toVC.attachments[toVC.startIndex]
let (sourceFrame, sourceCornerRadius) = toVC.sourcesInfo[toVC.startIndex]
let finalVCFrame = transitionContext.finalFrame(for: toVC) let finalVCFrame = transitionContext.finalFrame(for: toVC)
guard let (sourceFrame, sourceCornerRadius) = toVC.sourcesInfo[toVC.startIndex] else {
toVC.view.frame = finalVCFrame
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return
}
let attachment = toVC.attachments[toVC.startIndex]
guard let data = ImageCache.attachments.get(attachment.url), let image = UIImage(data: data) else { guard let data = ImageCache.attachments.get(attachment.url), let image = UIImage(data: data) else {
toVC.view.frame = finalVCFrame toVC.view.frame = finalVCFrame

View File

@ -26,7 +26,10 @@ class GalleryShrinkAnimationController: NSObject, UIViewControllerAnimatedTransi
return return
} }
let (sourceFrame, sourceCornerRadius) = fromVC.sourcesInfo[fromVC.currentIndex] guard let (sourceFrame, sourceCornerRadius) = fromVC.sourcesInfo[fromVC.currentIndex] else {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return
}
let originalVCFrame = fromVC.view.frame let originalVCFrame = fromVC.view.frame
let attachment = fromVC.attachments[fromVC.currentIndex] let attachment = fromVC.attachments[fromVC.currentIndex]

View File

@ -13,8 +13,9 @@ import Gifu
class LargeImageViewController: UIViewController, UIScrollViewDelegate { class LargeImageViewController: UIViewController, UIScrollViewDelegate {
var originFrame: CGRect? typealias SourceInfo = (frame: CGRect, cornerRadius: CGFloat)
var originCornerRadius: CGFloat?
var sourceInfo: SourceInfo?
var dismissInteractionController: LargeImageInteractionController? var dismissInteractionController: LargeImageInteractionController?
@IBOutlet weak var scrollView: UIScrollView! @IBOutlet weak var scrollView: UIScrollView!
@ -60,11 +61,10 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate {
return !controlsVisible return !controlsVisible
} }
init(image: UIImage, description: String?, sourceFrame: CGRect?, sourceCornerRadius: CGFloat?) { init(image: UIImage, description: String?, sourceInfo: SourceInfo?) {
self.image = image self.image = image
self.imageDescription = description self.imageDescription = description
self.originFrame = sourceFrame self.sourceInfo = sourceInfo
self.originCornerRadius = sourceCornerRadius
super.init(nibName: "LargeImageViewController", bundle: nil) super.init(nibName: "LargeImageViewController", bundle: nil)

View File

@ -17,13 +17,18 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from), guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to) as? LargeImageViewController, let toVC = transitionContext.viewController(forKey: .to) as? LargeImageViewController else {
let originFrame = toVC.originFrame else {
return return
} }
let containerView = transitionContext.containerView
let finalVCFrame = transitionContext.finalFrame(for: toVC) let finalVCFrame = transitionContext.finalFrame(for: toVC)
guard let (originFrame, originCornerRadius) = toVC.sourceInfo else {
toVC.view.frame = finalVCFrame
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return
}
let containerView = transitionContext.containerView
let image = toVC.imageView.image! let image = toVC.imageView.image!
let ratio = image.size.width / image.size.height let ratio = image.size.width / image.size.height
let width = finalVCFrame.width let width = finalVCFrame.width
@ -36,7 +41,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
imageView.animate(withGIFData: gifData) imageView.animate(withGIFData: gifData)
} }
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = toVC.originCornerRadius! imageView.layer.cornerRadius = originCornerRadius
imageView.layer.masksToBounds = true imageView.layer.masksToBounds = true
let blackView = UIView(frame: finalVCFrame) let blackView = UIView(frame: finalVCFrame)

View File

@ -23,11 +23,15 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from) as? LargeImageViewController, guard let fromVC = transitionContext.viewController(forKey: .from) as? LargeImageViewController,
let toVC = transitionContext.viewController(forKey: .to), let toVC = transitionContext.viewController(forKey: .to) else {
let finalFrame = fromVC.originFrame else {
return return
} }
guard let (finalFrame, finalCornerRadius) = fromVC.sourceInfo else {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return
}
let originalVCFrame = fromVC.view.frame let originalVCFrame = fromVC.view.frame
let containerView = transitionContext.containerView let containerView = transitionContext.containerView
@ -57,7 +61,7 @@ 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 = finalFrame imageView.frame = finalFrame
imageView.layer.cornerRadius = fromVC.originCornerRadius! imageView.layer.cornerRadius = finalCornerRadius
blackView.alpha = 0 blackView.alpha = 0
}, completion: { _ in }, completion: { _ in
blackView.removeFromSuperview() blackView.removeFromSuperview()

View File

@ -34,9 +34,9 @@ protocol TuskerNavigationDelegate {
func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIView) func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIView)
func gallery(attachments: [Attachment], sourceViews: [UIView], startIndex: Int) -> GalleryViewController func gallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) -> GalleryViewController
func showGallery(attachments: [Attachment], sourceViews: [UIView], startIndex: Int) func showGallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int)
func showMoreOptions(forStatus statusID: String) func showMoreOptions(forStatus statusID: String)
@ -99,7 +99,9 @@ extension TuskerNavigationDelegate where Self: UIViewController {
present(vc, animated: true) present(vc, animated: true)
} }
private func sourceViewInfo(_ sourceView: UIView) -> (CGRect, CGFloat) { private func sourceViewInfo(_ sourceView: UIView?) -> LargeImageViewController.SourceInfo? {
guard let sourceView = sourceView else { return nil }
var sourceFrame = sourceView.convert(sourceView.bounds, to: view) var sourceFrame = sourceView.convert(sourceView.bounds, to: view)
if let scrollView = view as? UIScrollView { if let scrollView = view as? UIScrollView {
let scale = scrollView.zoomScale let scale = scrollView.zoomScale
@ -109,19 +111,17 @@ extension TuskerNavigationDelegate where Self: UIViewController {
let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY
sourceFrame = CGRect(x: x, y: y, width: width, height: height) sourceFrame = CGRect(x: x, y: y, width: width, height: height)
} }
return (sourceFrame, sourceView.layer.cornerRadius) return (frame: sourceFrame, cornerRadius: sourceView.layer.cornerRadius)
} }
func largeImage(_ image: UIImage, description: String?, sourceView: UIView) -> LargeImageViewController { func largeImage(_ image: UIImage, description: String?, sourceView: UIView) -> LargeImageViewController {
let (sourceFrame, sourceCornerRadius) = sourceViewInfo(sourceView) let vc = LargeImageViewController(image: image, description: description, sourceInfo: sourceViewInfo(sourceView))
let vc = LargeImageViewController(image: image, description: description, sourceFrame: sourceFrame, sourceCornerRadius: sourceCornerRadius)
vc.transitioningDelegate = self vc.transitioningDelegate = self
return vc return vc
} }
func largeImage(gifData: Data, description: String?, sourceView: UIView) -> LargeImageViewController { func largeImage(gifData: Data, description: String?, sourceView: UIView) -> LargeImageViewController {
let (sourceFrame, sourceCornerRadius) = sourceViewInfo(sourceView) let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceInfo: sourceViewInfo(sourceView))
let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceFrame: sourceFrame, sourceCornerRadius: sourceCornerRadius)
vc.transitioningDelegate = self vc.transitioningDelegate = self
vc.gifData = gifData vc.gifData = gifData
return vc return vc
@ -135,14 +135,14 @@ extension TuskerNavigationDelegate where Self: UIViewController {
present(largeImage(gifData: gifData, description: description, sourceView: sourceView), animated: true) present(largeImage(gifData: gifData, description: description, sourceView: sourceView), animated: true)
} }
func gallery(attachments: [Attachment], sourceViews: [UIView], startIndex: Int) -> GalleryViewController { func gallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) -> GalleryViewController {
let sourcesInfo = sourceViews.map(sourceViewInfo) let sourcesInfo = sourceViews.map(sourceViewInfo)
let vc = GalleryViewController(attachments: attachments, sourcesInfo: sourcesInfo, startIndex: startIndex) let vc = GalleryViewController(attachments: attachments, sourcesInfo: sourcesInfo, startIndex: startIndex)
vc.transitioningDelegate = self vc.transitioningDelegate = self
return vc return vc
} }
func showGallery(attachments: [Attachment], sourceViews: [UIView], startIndex: Int) { func showGallery(attachments: [Attachment], sourceViews: [UIView?], startIndex: Int) {
present(gallery(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex), animated: true) present(gallery(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex), animated: true)
} }

View File

@ -13,60 +13,79 @@ class AttachmentsContainerView: UIView {
var delegate: AttachmentViewDelegate? var delegate: AttachmentViewDelegate?
let attachmentViews: NSHashTable<AttachmentView> = .weakObjects()
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
self.isUserInteractionEnabled = true self.isUserInteractionEnabled = true
} }
func getAttachmentView(for attachment: Attachment) -> AttachmentView? {
return attachmentViews.allObjects.first { $0.attachment.id == attachment.id }
}
// MARK: - User Interaface
func updateUI(status: Status) { func updateUI(status: Status) {
let attachments = status.attachments.filter { $0.kind == .image } let attachments = status.attachments.filter { $0.kind == .image }
attachmentViews.removeAllObjects()
subviews.forEach { $0.removeFromSuperview() }
if attachments.count > 0 { if attachments.count > 0 {
self.isHidden = false self.isHidden = false
let mainView: UIView
switch attachments.count { switch attachments.count {
case 1: case 1:
mainView = createAttachmentView(attachments[0]) makeMainView(createAttachmentView(attachments[0]))
case 2: case 2:
mainView = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [ let left = createAttachmentView(attachments[0])
createAttachmentView(attachments[0]), makeMainView(createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
left,
createAttachmentView(attachments[1]) createAttachmentView(attachments[1])
]))
NSLayoutConstraint.activate([
left.halfWidth()
]) ])
case 3: case 3:
mainView = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [ let left = createAttachmentView(attachments[0])
createAttachmentView(attachments[0]), let topRight = createAttachmentView(attachments[1])
makeMainView(createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
left,
createAttachmentsStack(axis: .vertical, arrangedSubviews: [ createAttachmentsStack(axis: .vertical, arrangedSubviews: [
createAttachmentView(attachments[1]), topRight,
createAttachmentView(attachments[2]) createAttachmentView(attachments[2])
]) ])
]))
NSLayoutConstraint.activate([
left.halfWidth(),
topRight.halfHeight(),
]) ])
case 4: case 4:
mainView = createAttachmentsStack(axis: .horizontal, arrangedSubviews: [ let topLeft = createAttachmentView(attachments[0])
let left = createAttachmentsStack(axis: .vertical, arrangedSubviews: [
topLeft,
createAttachmentView(attachments[2])
])
let topRight = createAttachmentView(attachments[1])
makeMainView(createAttachmentsStack(axis: .horizontal, arrangedSubviews: [
left,
createAttachmentsStack(axis: .vertical, arrangedSubviews: [ createAttachmentsStack(axis: .vertical, arrangedSubviews: [
createAttachmentView(attachments[0]), topRight,
createAttachmentView(attachments[2])
]),
createAttachmentsStack(axis: .vertical, arrangedSubviews: [
createAttachmentView(attachments[1]),
createAttachmentView(attachments[3]) createAttachmentView(attachments[3])
]) ])
]))
NSLayoutConstraint.activate([
left.halfWidth(),
topLeft.halfHeight(),
topRight.halfHeight(),
]) ])
default: default:
fatalError("Too many attachments") fatalError("Too many attachments")
} }
addSubview(mainView)
NSLayoutConstraint.activate([
mainView.leadingAnchor.constraint(equalTo: leadingAnchor),
mainView.trailingAnchor.constraint(equalTo: trailingAnchor),
mainView.topAnchor.constraint(equalTo: topAnchor),
mainView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
} else { } else {
self.isHidden = true self.isHidden = true
subviews.forEach { $0.removeFromSuperview() }
} }
} }
@ -74,15 +93,49 @@ class AttachmentsContainerView: UIView {
let attachmentView = AttachmentView(attachment: attachment) let attachmentView = AttachmentView(attachment: attachment)
attachmentView.delegate = delegate attachmentView.delegate = delegate
attachmentView.translatesAutoresizingMaskIntoConstraints = false attachmentView.translatesAutoresizingMaskIntoConstraints = false
attachmentViews.add(attachmentView)
return attachmentView return attachmentView
} }
private func createAttachmentsStack(axis: NSLayoutConstraint.Axis, arrangedSubviews: [UIView]) -> UIStackView { private func createAttachmentsStack(axis: NSLayoutConstraint.Axis, arrangedSubviews: [UIView]) -> UIStackView {
let stack = UIStackView(arrangedSubviews: arrangedSubviews) let stack = UIStackView(arrangedSubviews: arrangedSubviews)
stack.axis = axis stack.axis = axis
stack.spacing = 8 stack.spacing = 4
stack.translatesAutoresizingMaskIntoConstraints = false stack.translatesAutoresizingMaskIntoConstraints = false
return stack return stack
} }
private func makeMainView(_ view: UIView) {
addSubview(view)
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: leadingAnchor),
view.trailingAnchor.constraint(equalTo: trailingAnchor),
view.topAnchor.constraint(equalTo: topAnchor),
view.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
fileprivate extension UIView {
enum RelativeSize {
case full, half
var multiplier: CGFloat {
switch self {
case .full:
return 1
case .half:
return 0.5
}
}
}
func halfWidth(spacing: CGFloat = 4) -> NSLayoutConstraint {
return widthAnchor.constraint(equalTo: superview!.widthAnchor, multiplier: 0.5, constant: -spacing / 2)
}
func halfHeight(spacing: CGFloat = 4) -> NSLayoutConstraint {
return heightAnchor.constraint(equalTo: superview!.heightAnchor, multiplier: 0.5, constant: -spacing / 2)
}
} }

View File

@ -307,9 +307,7 @@ extension StatusTableViewCell: AttachmentViewDelegate {
func showLargeAttachment(for attachmentView: AttachmentView) { func showLargeAttachment(for attachmentView: AttachmentView) {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let startIndex = status.attachments.firstIndex { $0.id == attachmentView.attachment.id } ?? 0 let startIndex = status.attachments.firstIndex { $0.id == attachmentView.attachment.id } ?? 0
let sourceViews = status.attachments.map { attachment in let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:))
attachmentsView.subviews.first { ($0 as! AttachmentView).attachment.id == attachment.id }!
}
delegate?.showGallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: startIndex) delegate?.showGallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: startIndex)
} }
} }