Fix attachments collection view sizing

This commit is contained in:
Shadowfacts 2024-12-17 15:23:01 -05:00
parent 96fdef0558
commit bee3e53be7

View File

@ -13,17 +13,35 @@ import InstanceFeatures
struct AttachmentsSection: View { struct AttachmentsSection: View {
@ObservedObject var draft: Draft @ObservedObject var draft: Draft
private let spacing: CGFloat = 8
private let minItemSize: CGFloat = 100
var body: some View { var body: some View {
#if os(visionOS)
collectionView
#else
if #available(iOS 16.0, *) {
collectionView
} else {
LegacyCollectionViewSizingView {
collectionView
} computeHeight: { width in
WrappedCollectionView.totalHeight(width: width, minItemSize: minItemSize, spacing: spacing, items: draft.attachments.count + 1)
}
}
#endif
}
private var collectionView: some View {
WrappedCollectionView( WrappedCollectionView(
draft: draft, draft: draft,
spacing: 8, spacing: spacing,
minItemSize: 100 minItemSize: minItemSize
) )
// Impose a minimum height, because otherwise it defaults to zero which prevents the collection // Impose a minimum height, because otherwise it defaults to zero which prevents the collection
// view from laying out, and leaving the intrinsic content size at zero too. // view from laying out, and leaving the intrinsic content size at zero too.
// Add 4 to the minItemSize because otherwise drag-and-drop while reordering can alter the contentOffset by that much. // Add 4 to the minItemSize because otherwise drag-and-drop while reordering can alter the contentOffset by that much.
.frame(minHeight: 104) .frame(minHeight: minItemSize + 4)
} }
static func insertAttachments(in draft: Draft, at index: Int, itemProviders: [NSItemProvider]) { static func insertAttachments(in draft: Draft, at index: Int, itemProviders: [NSItemProvider]) {
@ -40,6 +58,41 @@ struct AttachmentsSection: View {
} }
} }
#if !os(visionOS)
@available(iOS, obsoleted: 16.0)
private struct LegacyCollectionViewSizingView<Content: View>: View {
@ViewBuilder let content: Content
let computeHeight: (CGFloat) -> CGFloat
@State private var width: CGFloat = 0
var body: some View {
let height = computeHeight(width)
content
.frame(height: max(height, 10))
.overlay {
GeometryReader { proxy in
Color.clear
.preference(key: WidthPrefKey.self, value: proxy.size.width)
.onPreferenceChange(WidthPrefKey.self) {
width = $0
}
}
}
}
}
private struct WidthPrefKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
let next = nextValue()
if next != 0 {
value = next
}
}
}
#endif
// Use a UIViewControllerRepresentable so we have something from which to present the gallery VC. // Use a UIViewControllerRepresentable so we have something from which to present the gallery VC.
private struct WrappedCollectionView: UIViewControllerRepresentable { private struct WrappedCollectionView: UIViewControllerRepresentable {
@ObservedObject var draft: Draft @ObservedObject var draft: Draft
@ -59,6 +112,51 @@ private struct WrappedCollectionView: UIViewControllerRepresentable {
} }
uiViewController.updateAttachments() uiViewController.updateAttachments()
} }
@available(iOS 16.0, *)
func sizeThatFits(_ proposal: ProposedViewSize, uiViewController: WrappedCollectionViewController, context: Context) -> CGSize? {
guard let width = proposal.width,
width.isFinite else {
return nil
}
let count = draft.attachments.count + 1
return CGSize(
width: width,
height: Self.totalHeight(width: width, minItemSize: minItemSize, spacing: spacing, items: count)
)
}
fileprivate static func itemSize(width: CGFloat, minItemSize: CGFloat, spacing: CGFloat) -> (CGFloat, Int) {
// The maximum item size is 2*minItemSize + spacing - 1,
// in the case where one item fits in the row but we are one pt short of
// adding a second item.
var itemSize = minItemSize
var fittingCount = floor((width + spacing) / (itemSize + spacing))
var usedSpaceForFittingCount = fittingCount * itemSize + (fittingCount - 1) * spacing
var remainingSpace = width - usedSpaceForFittingCount
if fittingCount == 0 {
return (0, 0)
} else if fittingCount == 1 && remainingSpace > minItemSize / 2 {
// If there's only one item that would fit at min size, and giving
// it the rest of the space would increase it by at least 50%,
// add a second item anywyas.
itemSize = (width - spacing) / 2
fittingCount = 2
usedSpaceForFittingCount = fittingCount * itemSize + (fittingCount - 1) * spacing
remainingSpace = width - usedSpaceForFittingCount
}
itemSize = itemSize + remainingSpace / fittingCount
return (itemSize, Int(fittingCount))
}
fileprivate static func totalHeight(width: CGFloat, minItemSize: CGFloat, spacing: CGFloat, items: Int) -> CGFloat {
let (size, itemsPerRow) = itemSize(width: width, minItemSize: minItemSize, spacing: spacing)
guard itemsPerRow != 0 else {
return 0
}
let rows = ceil(Double(items) / Double(itemsPerRow))
return size * rows + spacing * (rows - 1)
}
} }
private class WrappedCollectionViewController: UIViewController { private class WrappedCollectionViewController: UIViewController {
@ -70,6 +168,10 @@ private class WrappedCollectionViewController: UIViewController {
fileprivate var currentInteractiveMoveCell: HostingCollectionViewCell? fileprivate var currentInteractiveMoveCell: HostingCollectionViewCell?
fileprivate var addAttachment: ((DraftAttachment) -> Void)? = nil fileprivate var addAttachment: ((DraftAttachment) -> Void)? = nil
var collectionView: UICollectionView {
view as! UICollectionView
}
init(spacing: CGFloat, minItemSize: CGFloat) { init(spacing: CGFloat, minItemSize: CGFloat) {
self.spacing = spacing self.spacing = spacing
self.minItemSize = minItemSize self.minItemSize = minItemSize
@ -82,7 +184,7 @@ private class WrappedCollectionViewController: UIViewController {
override func loadView() { override func loadView() {
let layout = UICollectionViewCompositionalLayout { [unowned self] section, environment in let layout = UICollectionViewCompositionalLayout { [unowned self] section, environment in
let (itemSize, itemsPerRow) = self.itemSize(width: environment.container.contentSize.width) let (itemSize, itemsPerRow) = WrappedCollectionView.itemSize(width: environment.container.contentSize.width, minItemSize: minItemSize, spacing: spacing)
let items = Array(repeating: NSCollectionLayoutItem(layoutSize: .init(widthDimension: .absolute(itemSize), heightDimension: .absolute(itemSize))), count: itemsPerRow) let items = Array(repeating: NSCollectionLayoutItem(layoutSize: .init(widthDimension: .absolute(itemSize), heightDimension: .absolute(itemSize))), count: itemsPerRow)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(itemSize)), subitems: items) let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(itemSize)), subitems: items)
@ -91,10 +193,12 @@ private class WrappedCollectionViewController: UIViewController {
section.interGroupSpacing = spacing section.interGroupSpacing = spacing
return section return section
} }
let attachmentCell = UICollectionView.CellRegistration<HostingCollectionViewCell, DraftAttachment> { cell, indexPath, attachment in let attachmentCell = UICollectionView.CellRegistration<HostingCollectionViewCell, DraftAttachment> { [unowned self] cell, indexPath, attachment in
cell.containingViewController = self
cell.setView(AttachmentCollectionViewCellView(attachment: attachment)) cell.setView(AttachmentCollectionViewCellView(attachment: attachment))
} }
let addButtonCell = UICollectionView.CellRegistration<HostingCollectionViewCell, Bool> { [unowned self] cell, indexPath, item in let addButtonCell = UICollectionView.CellRegistration<HostingCollectionViewCell, Bool> { [unowned self] cell, indexPath, item in
cell.containingViewController = self
cell.setView(AddAttachmentButton(viewController: self, enabled: item)) cell.setView(AddAttachmentButton(viewController: self, enabled: item))
} }
let collectionView = IntrinsicContentSizeCollectionView(frame: .zero, collectionViewLayout: layout) let collectionView = IntrinsicContentSizeCollectionView(frame: .zero, collectionViewLayout: layout)
@ -140,27 +244,6 @@ private class WrappedCollectionViewController: UIViewController {
collectionView.addGestureRecognizer(longPressRecognizer) collectionView.addGestureRecognizer(longPressRecognizer)
} }
private func itemSize(width: CGFloat) -> (CGFloat, Int) {
// The maximum item size is 2*minItemSize + spacing - 1,
// in the case where one item fits in the row but we are one pt short of
// adding a second item.
var itemSize = minItemSize
var fittingCount = floor((width + spacing) / (itemSize + spacing))
var usedSpaceForFittingCount = fittingCount * itemSize + (fittingCount - 1) * spacing
var remainingSpace = width - usedSpaceForFittingCount
if fittingCount == 1 && remainingSpace > minItemSize / 2 {
// If there's only one item that would fit at min size, and giving
// it the rest of the space would increase it by at least 50%,
// add a second item anywyas.
itemSize = (width - spacing) / 2
fittingCount = 2
usedSpaceForFittingCount = fittingCount * itemSize + (fittingCount - 1) * spacing
remainingSpace = width - usedSpaceForFittingCount
}
itemSize = itemSize + remainingSpace / fittingCount
return (itemSize, Int(fittingCount))
}
func updateAttachments() { func updateAttachments() {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.all]) snapshot.appendSections([.all])
@ -300,6 +383,8 @@ private class HostingCollectionViewCell: UICollectionViewCell {
#else #else
@available(iOS, obsoleted: 16.0) @available(iOS, obsoleted: 16.0)
private class HostingCollectionViewCell: UICollectionViewCell { private class HostingCollectionViewCell: UICollectionViewCell {
weak var containingViewController: UIViewController?
@available(iOS, obsoleted: 16.0) @available(iOS, obsoleted: 16.0)
private var hostController: UIHostingController<AnyView>? private var hostController: UIHostingController<AnyView>?
private(set) var hostView: UIView? private(set) var hostView: UIView?
@ -324,11 +409,12 @@ private class HostingCollectionViewCell: UICollectionViewCell {
hostController.rootView = AnyView(view) hostController.rootView = AnyView(view)
} else { } else {
let host = UIHostingController(rootView: AnyView(view)) let host = UIHostingController(rootView: AnyView(view))
containingViewController!.addChild(host)
host.view.frame = contentView.bounds host.view.frame = contentView.bounds
contentView.addSubview(host.view) contentView.addSubview(host.view)
host.didMove(toParent: containingViewController!)
hostController = host hostController = host
hostView = host.view hostView = host.view
// drop the hosting controller on the floor and hope nothing bad happens
} }
} }
} }