ekazaev / ChatLayout

ChatLayout is an alternative solution to MessageKit. It uses custom UICollectionViewLayout to provide you full control over the presentation as well as all the tools available in UICollectionView. It supports dynamic cells and supplementary view sizes.
MIT License
896 stars 67 forks source link

Adaptation AppKit #78

Open Mx-Iris opened 2 months ago

Mx-Iris commented 2 months ago

Thank you very much for creating this repository, I really like to customize the collectionView using layout, UICollectionView and NSCollectionView are two very similar components, I did some adaptations to make it work on Mac, I'm going to write some test cases next, I have initially tested it to be able to run!

ekazaev commented 2 months ago

@Mx-Iris Hey. Great job. Thank you as well. Ill review it later this week. But by when I see looks great.

Mx-Iris commented 2 months ago

@ekazaev Thank you very much for your recognition. I think the main library is almost done. The example app is still being adapted and is expected to be completed this week. As for the documentation, you may need to make some modifications.

Mx-Iris commented 2 months ago

@ekazaev I renamed all types with the UI prefix to NSUI today, without the prefix it might conflict with SwiftUI's type

Mx-Iris commented 2 months ago

@ekazaev I found a very serious problem, the targetContentOffsetForProposedContentOffset method of NSCollectionViewLayout does not work, the returned offset is not applied, and the visibleBounds will be incorrect after a while of forced scrolling to the specified position, so keepContentOffsetAtBottomOnBatchUpdates this function is currently ineffective, do you have any better suggestions?

Mx-Iris commented 2 months ago

Here's a log, it just started working fine, but at one point the proposedContentOffset was less than the previous newProposedContentOffset, which seems to be the value of visibleBounds, and I'm now forcing the contentOffset to be set in the same way as the fix for iOS15, as the Returning directly doesn't work, and by the way, Catalyst doesn't seem to need this setting.

targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 44.0
proposedContentOffset: (0.0, 1692.0), newProposedContentOffset: (0.0, 1736.0)
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 44.0
proposedContentOffset: (0.0, 1736.0), newProposedContentOffset: (0.0, 1780.0)
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 44.0
proposedContentOffset: (0.0, 1764.0), newProposedContentOffset: (0.0, 1808.0)
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 44.0
proposedContentOffset: (0.0, 1800.0), newProposedContentOffset: (0.0, 1844.0)
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 44.0
proposedContentOffset: (0.0, 1840.0), newProposedContentOffset: (0.0, 1884.0)
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 44.0
proposedContentOffset: (0.0, 1848.0), newProposedContentOffset: (0.0, 1892.0)
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
Mx-Iris commented 2 months ago

This is the log that doesn't force the contentOffset to be set

itemFrameMaxY: 3191.0, visibleBoundsMaxY: 3199.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 44.0
proposedContentOffset: (0.0, 2431.0), newProposedContentOffset: (0.0, 2475.0)
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.5
proposedContentOffset: (0.0, 2471.0), newProposedContentOffset: (0.0, 2471.5)
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.5
proposedContentOffset: (0.0, 2471.0), newProposedContentOffset: (0.0, 2471.5)
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.5
proposedContentOffset: (0.0, 2471.0), newProposedContentOffset: (0.0, 2471.5)
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.5
proposedContentOffset: (0.0, 2471.0), newProposedContentOffset: (0.0, 2471.5)
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.5
proposedContentOffset: (0.0, 2471.0), newProposedContentOffset: (0.0, 2471.5)
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.5
proposedContentOffset: (0.0, 2471.0), newProposedContentOffset: (0.0, 2471.5)
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.5
proposedContentOffset: (0.0, 2471.0), newProposedContentOffset: (0.0, 2471.5)
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.5
proposedContentOffset: (0.0, 2471.0), newProposedContentOffset: (0.0, 2471.5)
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.5
proposedContentOffset: (0.0, 2471.0), newProposedContentOffset: (0.0, 2471.5)
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.5
proposedContentOffset: (0.0, 2471.0), newProposedContentOffset: (0.0, 2471.5)
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.5
proposedContentOffset: (0.0, 2471.0), newProposedContentOffset: (0.0, 2471.5)
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.0
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.5
proposedContentOffset: (0.0, 2471.0), newProposedContentOffset: (0.0, 2471.5)
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.5
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.5
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.5
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.5
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.5
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.5
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
itemFrameMaxY: 3236.5, visibleBoundsMaxY: 3239.5
targetContentOffset(forProposedContentOffset:), controller.proposedCompensatingOffset: 0.0
Mx-Iris commented 2 months ago

Based on a series of reverse engineering, I found that after the method targetContentOffsetForProposedContentOffset: of NSCollectionViewLayout is called and returns, _NSCollectionViewCore sets its own bounds. However, in UIKit, it is UICollectionView that calls this method and similarly sets its own bounds. Since UICollectionView itself is a UIScrollView, the contentOffset is correctly set. But I found that _NSCollectionViewCore seems not to set the origin of NSClipView or call the collectionView's scroll method. We can manually set the contentOffset before this method returns.

Another issue is that, after this series of methods ends, calling invalidateLayout, and then returning to the initial method _updateWithItems:tentativelyForReordering:..., to update and create a series of layoutAttributes. The problem is that in UIKit, the frame.y/height of the two layoutAttributes in the method invalidationContextForPreferredLayoutAttributes:withOriginalAttributes: are different, whereas in AppKit, they are exactly the same, leading to the contentOffsetAdjustment in the context not being set and remaining at 0. I feel the biggest issue currently lies here.

Welcome to discuss together, there is very little information and documentation about CollectionViewLayout, and a lot of reverse engineering is needed to gain insights.

ekazaev commented 1 month ago

@Mx-Iris Hi. I am currently on my vacation. So sorry for making little input here. I actually know very little about AppKit since I never worked with the Mac applications. I am not sure If I can be a good help there :(

Mx-Iris commented 1 month ago

@ekazaev It's okay. Just leave it to me.

Mx-Iris commented 1 month ago

@ekazaev Hi, I'm sorry for not updating the progress for so long. I've been very busy with work lately. I've been researching for a long time without any progress, so I've put it on hold for now. I tried submitting a DTS to Apple, but they asked me to provide a reproducible demo. I don't have time to make these.

Currently, it seems that only this function has problems. You can review it first and let's consider these later.

Mx-Iris commented 1 month ago

@ekazaev Because AppKit cannot customize layout margins, the demo looks a bit strange. I will make time to finish them.

Mx-Iris commented 1 month ago

I tried to extend NSView with customizable layoutMargins, and the UI looks fine now. However, I found that the feature of scrolling to the bottom at launch in the demo is not working properly, so I am looking for a solution.

ekazaev commented 1 week ago

@Mx-Iris Good luck. Emulate also pushing into the navigation controller. It also usually shows issues.