Skip to content

Commit

Permalink
Added support for UICollectionView.selfSizingInvalidation.
Browse files Browse the repository at this point in the history
  • Loading branch information
ekazaev committed Nov 3, 2023
1 parent 9bbacaf commit 6c4676f
Show file tree
Hide file tree
Showing 84 changed files with 392 additions and 237 deletions.
2 changes: 1 addition & 1 deletion ChatLayout.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'ChatLayout'
s.version = '2.0.2'
s.version = '2.0.3'
s.summary = 'Chat UI Library. It uses custom UICollectionViewLayout to provide you full control over the presentation.'
s.swift_version = '5.8'

Expand Down
23 changes: 22 additions & 1 deletion ChatLayout/Classes/Core/CollectionViewChatLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
/// This flag is only to provide fine control over the batch updates. If in doubts - keep it `true`.
public var processOnlyVisibleItemsOnAnimatedBatchUpdates: Bool = true

/// A mode that enables automatic self-sizing invalidation after Auto Layout changes. It's advisable to continue using the reload/reconfigure method, especially when multiple
/// changes occur concurrently in an animated fashion. This approach ensures that the `CollectionViewChatLayout` can handle these changes while maintaining the content offset accurately.
/// Consider using it when no better alternatives are available.
///
/// **NB:**
/// This is an experimental flag.
@available(iOS 16.0, *)
public var supportSelfSizingInvalidation: Bool {
get {
_supportSelfSizingInvalidation
}
set {
_supportSelfSizingInvalidation = newValue
}
}

/// Represent the currently visible rectangle.
open var visibleBounds: CGRect {
guard let collectionView else {
Expand Down Expand Up @@ -199,6 +215,8 @@ open class CollectionViewChatLayout: UICollectionViewLayout {

private var reconfigureItemsIndexPaths: [IndexPath] = []

private var _supportSelfSizingInvalidation: Bool = false

// MARK: IOS 15.1 fix flags

private var needsIOS15_1IssueFix: Bool {
Expand Down Expand Up @@ -509,7 +527,10 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
return true
}

let shouldInvalidateLayout = item.calculatedSize == nil || item.alignment != preferredMessageAttributes.alignment || item.interItemSpacing != preferredMessageAttributes.interItemSpacing
let shouldInvalidateLayout = item.calculatedSize == nil
|| (_supportSelfSizingInvalidation ? (item.size.height - preferredMessageAttributes.size.height).rounded() != 0 : false)
|| item.alignment != preferredMessageAttributes.alignment
|| item.interItemSpacing != preferredMessageAttributes.interItemSpacing

return shouldInvalidateLayout
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// https://github.com/sponsors/ekazaev
//

import ChatLayout
import DifferenceKit
import Foundation
import UIKit
Expand Down Expand Up @@ -88,9 +89,16 @@ public extension UICollectionView {
}

if !changeset.elementUpdated.isEmpty {
reloadItems(at: changeset.elementUpdated.map {
let indexPaths = changeset.elementUpdated.map {
IndexPath(item: $0.element, section: $0.section)
})
}
if #available(iOS 15.0, *),
enableReconfigure {
reconfigureItems(at: indexPaths)
(collectionViewLayout as? CollectionViewChatLayout)?.reconfigureItems(at: indexPaths)
} else {
reloadItems(at: indexPaths)
}
}

for (source, target) in changeset.elementMoved {
Expand Down
20 changes: 10 additions & 10 deletions Example/ChatLayout/Chat/Model/Caches.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ import Foundation

let loader = CachingImageLoader(cache: imageCache, loader: DefaultImageLoader())

@available(iOS 13, *)
var metadataCache = IterativeCache(mainCache: MetaDataCache(cache: MemoryDataCache<URL>()),
backupCache: MetaDataCache(cache: PersistentDataCache<URL>(cacheFileExtension: "metadataCache")))

let imageCache = IterativeCache(mainCache: ImageForUrlCache(cache: MemoryDataCache<CacheableImageKey>()),
backupCache: ImageForUrlCache(cache: PersistentDataCache<CacheableImageKey>()))

// Uncomment to reload dynamic content on every start.
// @available(iOS 13, *)
// var metadataCache = MetaDataCache(cache: MemoryDataCache<URL>())
// var metadataCache = IterativeCache(mainCache: MetaDataCache(cache: MemoryDataCache<URL>()),
// backupCache: MetaDataCache(cache: PersistentDataCache<URL>(cacheFileExtension: "metadataCache")))
//
// let imageCache = ImageForUrlCache(cache: MemoryDataCache<CacheableImageKey>())
// let imageCache = IterativeCache(mainCache: ImageForUrlCache(cache: MemoryDataCache<CacheableImageKey>()),
// backupCache: ImageForUrlCache(cache: PersistentDataCache<CacheableImageKey>()))

// Uncomment to reload dynamic content on every start.
@available(iOS 13, *)
var metadataCache = MetaDataCache(cache: MemoryDataCache<URL>())

let imageCache = ImageForUrlCache(cache: MemoryDataCache<CacheableImageKey>())
17 changes: 17 additions & 0 deletions Example/ChatLayout/Chat/View/Avatar View/AvatarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,17 @@ import ChatLayout
import Foundation
import UIKit

// Just to visually test `ChatLayout.supportSelfSizingInvalidation`
protocol AvatarViewDelegate: AnyObject {

func avatarTapped()

}

final class AvatarView: UIView, StaticViewFactory {

weak var delegate: AvatarViewDelegate?

private lazy var circleImageView = RoundedCornersContainerView<UIImageView>(frame: bounds)

private var controller: AvatarViewController?
Expand Down Expand Up @@ -63,6 +72,14 @@ final class AvatarView: UIView, StaticViewFactory {
circleImageView.heightAnchor.constraint(equalTo: circleImageView.widthAnchor, multiplier: 1).isActive = true

circleImageView.customView.contentMode = .scaleAspectFill

let gestureRecogniser = UITapGestureRecognizer()
circleImageView.addGestureRecognizer(gestureRecogniser)
gestureRecogniser.addTarget(self, action: #selector(avatarTapped))
}

@objc private func avatarTapped() {
delegate?.avatarTapped()
}

}
14 changes: 14 additions & 0 deletions Example/ChatLayout/Chat/View/ChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ import FPSCounter
import InputBarAccessoryView
import UIKit

// It's advisable to continue using the reload/reconfigure method, especially when multiple changes occur concurrently in an animated fashion.
// This approach ensures that the ChatLayout can handle these changes while maintaining the content offset accurately.
// Consider using it when no better alternatives are available.
let enableSelfSizingSupport = false

// By setting this flag to true you can test reconfigure instead of reload.
let enableReconfigure = false

final class ChatViewController: UIViewController {

private enum ReactionTypes {
Expand Down Expand Up @@ -143,6 +151,12 @@ final class ChatViewController: UIViewController {
collectionView.automaticallyAdjustsScrollIndicatorInsets = true
}

if #available(iOS 16.0, *),
enableSelfSizingSupport {
collectionView.selfSizingInvalidation = .enabled
chatLayout.supportSelfSizingInvalidation = true
}

collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.frame = view.bounds
NSLayoutConstraint.activate([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,25 +198,6 @@ final class DefaultChatCollectionDataSource: NSObject, ChatCollectionDataSource
}
}

private func setupCellLayoutView(_ cellView: CellLayoutContainerView<AvatarView, some Any, StatusView>,
user: User,
alignment: ChatItemAlignment,
bubble: Cell.BubbleType,
status: MessageStatus) {
cellView.alignment = .bottom
cellView.leadingView?.isHiddenSafe = !alignment.isIncoming
cellView.leadingView?.alpha = alignment.isIncoming ? 1 : 0
cellView.trailingView?.isHiddenSafe = alignment.isIncoming
cellView.trailingView?.alpha = alignment.isIncoming ? 0 : 1
cellView.trailingView?.setup(with: status)

if let avatarView = cellView.leadingView {
let avatarViewController = AvatarViewController(user: user, bubble: bubble)
avatarView.setup(with: avatarViewController)
avatarViewController.view = avatarView
}
}

private func setupMainMessageView(_ cellView: MainContainerView<AvatarView, some Any, StatusView>,
user: User,
alignment: ChatItemAlignment,
Expand All @@ -232,6 +213,11 @@ final class DefaultChatCollectionDataSource: NSObject, ChatCollectionDataSource
let avatarViewController = AvatarViewController(user: user, bubble: bubble)
avatarView.setup(with: avatarViewController)
avatarViewController.view = avatarView
if let avatarDelegate = cellView.customView.customView as? AvatarViewDelegate {
avatarView.delegate = avatarDelegate
} else {
avatarView.delegate = nil
}
}
}

Expand Down
17 changes: 13 additions & 4 deletions Example/ChatLayout/Chat/View/Image View/ImageController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ final class ImageController {

weak var view: ImageView? {
didSet {
view?.reloadData()
UIView.performWithoutAnimation {
view?.reloadData()
}
}
}

Expand Down Expand Up @@ -52,11 +54,18 @@ final class ImageController {
self.image = image
view?.reloadData()
} else {
loader.loadImage(from: url) { [weak self] _ in
guard let self else {
loader.loadImage(from: url) { [weak self] result in
guard let self,
case let .success(image) = result else {
return
}
delegate?.reloadMessage(with: messageId)
if #available(iOS 16.0, *),
enableSelfSizingSupport {
self.image = image
view?.reloadData()
} else {
delegate?.reloadMessage(with: messageId)
}
}
}
case let .image(image):
Expand Down
55 changes: 29 additions & 26 deletions Example/ChatLayout/Chat/View/Image View/ImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,33 +71,36 @@ final class ImageView: UIView, ContainerCollectionViewCellDelegate {
}

func reloadData() {
UIView.performWithoutAnimation {
switch controller.state {
case .loading:
loadingIndicator.isHidden = false
imageView.isHidden = true
imageView.image = nil
stackView.removeArrangedSubview(imageView)
stackView.addArrangedSubview(loadingIndicator)
if !loadingIndicator.isAnimating {
loadingIndicator.startAnimating()
}
if #available(iOS 13.0, *) {
backgroundColor = .systemGray5
} else {
backgroundColor = UIColor(red: 200 / 255, green: 200 / 255, blue: 200 / 255, alpha: 1)
}
setupSize()
case let .image(image):
loadingIndicator.isHidden = true
loadingIndicator.stopAnimating()
imageView.isHidden = false
imageView.image = image
stackView.removeArrangedSubview(loadingIndicator)
stackView.addArrangedSubview(imageView)
setupSize()
backgroundColor = .clear
switch controller.state {
case .loading:
loadingIndicator.isHidden = false
imageView.isHidden = true
imageView.image = nil
stackView.removeArrangedSubview(imageView)
stackView.addArrangedSubview(loadingIndicator)
if !loadingIndicator.isAnimating {
loadingIndicator.startAnimating()
}
if #available(iOS 13.0, *) {
backgroundColor = .systemGray5
} else {
backgroundColor = UIColor(red: 200 / 255, green: 200 / 255, blue: 200 / 255, alpha: 1)
}
setupSize()
case let .image(image):
loadingIndicator.isHidden = true
loadingIndicator.stopAnimating()
imageView.isHidden = false
imageView.image = image
stackView.removeArrangedSubview(loadingIndicator)
stackView.addArrangedSubview(imageView)
setupSize()
stackView.setNeedsLayout()
stackView.layoutIfNeeded()
backgroundColor = .clear
}
if let cell = superview(of: UICollectionViewCell.self) {
cell.contentView.invalidateIntrinsicContentSize()
}
}

Expand Down
8 changes: 8 additions & 0 deletions Example/ChatLayout/Chat/View/Other/UIView+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ import UIKit

extension UIView {

func superview<T>(of type: T.Type) -> T? {
superview as? T ?? superview.flatMap { $0.superview(of: type) }
}

func subview<T>(of type: T.Type) -> T? {
subviews.compactMap { $0 as? T ?? $0.subview(of: type) }.first
}

// Even though we do not set it animated - it can happen during the animated batch update
// http://www.openradar.me/25087688
// https://github.com/nkukushkin/StackView-Hiding-With-Animation-Bug-Example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ final class TextMessageView: UIView, ContainerCollectionViewCellDelegate {

}

extension TextMessageView: AvatarViewDelegate {
func avatarTapped() {
if enableSelfSizingSupport {
layoutMargins = layoutMargins == .zero ? UIEdgeInsets(top: 50, left: 0, bottom: 50, right: 0) : .zero
setNeedsLayout()
if let cell = superview(of: UICollectionViewCell.self) {
cell.contentView.invalidateIntrinsicContentSize()
}
}
}
}

/// UITextView with hacks to avoid selection
private final class MessageTextView: UITextView {

Expand Down
16 changes: 14 additions & 2 deletions Example/ChatLayout/Chat/View/URL View/URLController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ final class URLController {

weak var delegate: ReloadDelegate?

weak var view: URLView?
weak var view: URLView? {
didSet {
UIView.performWithoutAnimation {
view?.reloadData()
}
}
}

private let provider = LPMetadataProvider()

Expand Down Expand Up @@ -55,7 +61,13 @@ final class URLController {
guard let self else {
return
}
delegate?.reloadMessage(with: messageId)
if #available(iOS 16.0, *),
enableSelfSizingSupport {
self.metadata = metadata
view?.reloadData()
} else {
delegate?.reloadMessage(with: messageId)
}
}
}
}
Expand Down
Loading

0 comments on commit 6c4676f

Please sign in to comment.