[CRASH] Invalid batch updates detected
Opened this issue · 6 comments
During 6 months of testing I had no error / crashes using this SDK but recently (maybe with new Xcode version) I'm getting a crash when I'm initializing the main grid for recents with exactly the same code than previous months :
func initController(completion: ((Bool?) -> Void?)) {
if(gridController == nil) {
gridController = GiphyGridController()
gridController!.delegate = self
// space between cells
gridController!.cellPadding = 4.0
// the scroll direction of the grid
gridController!.direction = .vertical
// the number of "tracks" is the span count. it represents num columns for vertical grids & num rows for horizontal grids
gridController!.numberOfTracks = 3
// hide the checkered background for stickers if you'd like (true by default)
//gridController!.showCheckeredBackground = false
gridController!.view.backgroundColor = .clear
gridController!.theme = GiphyGridTheme()
// by default, the waterfall layout sizes cells according to the aspect ratio of the media
// the fixedSizeCells setting makes it so each cell is square
// this setting only applies to Stickers (not GIFs)
gridController!.fixedSizeCells = true
if(GPHRecents.count > 0) {
gridController!.content = GPHContent.recents
}
else {
gridController!.content = GPHContent.trending(mediaType: .sticker)
}
mainView!.addSubview(gridController!.view)
gridController!.view.translatesAutoresizingMaskIntoConstraints = false
gridController!.view.leftAnchor.constraint(equalTo: mainView!.safeLeftAnchor).isActive = true
gridController!.view.rightAnchor.constraint(equalTo: mainView!.safeRightAnchor).isActive = true
gridController!.view.topAnchor.constraint(equalTo: mainView!.safeTopAnchor).isActive = true
gridController!.view.bottomAnchor.constraint(equalTo: mainView!.safeBottomAnchor).isActive = true
gridController!.update()
completion(true)
}
else {
completion(false)
}
}
And then I'm getting this error :
*** Assertion failure in -[UICollectionView _Bug_Detected_In_Client_Of_UICollectionView_Invalid_Batch_Updates:], UICollectionView.m:10064
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid batch updates detected: the number of sections and/or items returned by the data source before and/or after performing the batch updates are inconsistent with the updates.
Data source before updates = { 1 section with item counts: [50] }
Data source after updates = { 1 section with item counts: [50] }
Updates = [
Insert item (0 - 0),
Insert item (0 - 1),
Insert item (0 - 2),
Insert item (0 - 3),
Insert item (0 - 4),
Insert item (0 - 5),
Insert item (0 - 6),
Insert item (0 - 7),
Insert item (0 - 8),
Insert item (0 - 9),
Insert item (0 - 10),
Insert item (0 - 11),
Insert item (0 - 12),
Insert item (0 - 13),
Insert item (0 - 14),
Insert item (0 - 15),
Insert item (0 - 16),
Insert item (0 - 17),
Insert item (0 - 18),
Insert item (0 - 19),
Insert item (0 - 20),
Insert item (0 - 21),
Insert item (0 - 22),
Insert item (0 - 23),
Insert item (0 - 24),
Insert item (0 - 25),
Insert item (0 - 26),
Insert item (0 - 27),
Insert item (0 - 28),
Insert item (0 - 29),
Insert item (0 - 30),
Insert item (0 - 31),
Insert item (0 - 32),
Insert item (0 - 33),
Insert item (0 - 34),
Insert item (0 - 35),
Insert item (0 - 36),
Insert item (0 - 37),
Insert item (0 - 38),
Insert item (0 - 39),
Insert item (0 - 40),
Insert item (0 - 41),
Insert item (0 - 42),
Insert item (0 - 43),
Insert item (0 - 44),
Insert item (0 - 45),
Insert item (0 - 46),
Insert item (0 - 47),
Insert item (0 - 48),
Insert item (0 - 49)
]
Collection view: <UICollectionView: 0x112e56400; frame = (0 0; 0 0); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x2810a2c40>; backgroundColor = UIExtendedGrayColorSpace 0 0; layer = <CALayer: 0x280523e80>; contentOffset: {0, 0}; contentSize: {0, 2448}; adjustedContentInset: {0, 0, 0, 0}; layout: <GiphyUISDK.GPHWaterfallLayout: 0x10acf6c00>; dataSource: <GiphyUISDK.GiphyGridController: 0x159c96a00>>'
*** First throw call stack:
(0x1b4e1ed94 0x1aded43d0 0x1af5c96cc 0x1b7585358 0x1b71905ec 0x1b70eeadc 0x10a0b9434 0x10a0c0148 0x10a0b9dcc 0x1b6eb3f58 0x10a0b8f44 0x10a06dca8 0x1bc2dc320 0x1bc2ddeac 0x1bc2ec6a4 0x1bc2ec2f4 0x1b4eadd18 0x1b4e8f650 0x1b4e944dc 0x1f00f435c 0x1b722037c 0x1b721ffe0 0x100eefa98 0x1d431cdec)
libc++abi: terminating due to uncaught exception of type NSException
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
frame #0: 0x00000001f3b28558 libsystem_kernel.dylib`__pthread_kill + 8
libsystem_kernel.dylib`:
-> 0x1f3b28558 <+8>: b.lo 0x1f3b28578 ; <+40>
0x1f3b2855c <+12>: pacibsp
0x1f3b28560 <+16>: stp x29, x30, [sp, #-0x10]!
0x1f3b28564 <+20>: mov x29, sp
Target 0: (Runner) stopped.
Lost connection to device.
- Why I am getting this crash now and not during previous month ?
- How can we fix this ?
Thanks in advance
hey @Nico3652 thanks for flagging this, sorry for the late reply. I'm using the latest Xcode from the App Store (14.3) and was unable to reproduce based on the code you shared, though I had to add self.addChild(gridController)
in order for it to work. Could you try just presenting the GiphyViewController
and let me know if that leads to a crash for you as well?
@cgmaier thanks for reply, I'm also using this version of Xcode.
I can confirm that this is not happening presenting the GiphyViewController
.
I can also confirm it never happen on the first opening : I have to present 3-5x my modal quickly to make it crash.
In order to add more context to my situation, I'm not building an iOS app in full swift code but a Flutter app mixing dart & swift.
I suppose you may be not an expert in flutter, but concepts are equals.
The main view I'm using is just a wrapper to insert the gridController :
mainView!.addSubview(gridController!.view)
It worked perfectly during months.
I cannot use self.addChild(gridController)
because the rootVC is in flutter. I'm only retrieving the grid view and add it in my view.
Do you think the problem could be linked to this ?
So I have done many tests and here is my workaround :
I have removed gridController!.update()
from my initController
and create another func
to update the grid with some delay (1s for now).
This workaround is working great for now until im able to understand this behavior and fix it.
hey @Nico3652 glad you found a solution. can you try creating a new GiphyGridController
every time? I think the issue may be related to using the same instance.
https://github.com/Giphy/giphy-ios-sdk/blob/main/Docs.md#giphygridcontroller-presentation
@cgmaier I followed the entire documentation,
I'm setting to nil
the instance every time the page is closed and recreate it inside the init
func and only if it's == to nil to avoid duplicate instance
are you also removing it from the view hierarchy?
@cgmaier indeed I'm removing it like this :
// Called when the modal is dismissed
func dismiss(completion: ((Bool?)->Void?)) {
if(gridController != nil) {
gridController!.view.removeFromSuperview()
}
gridController = nil
querySearch = nil
completion(true)
}
However I'm not an expert in Swift and iOS behavior, am I doing it wrong ?
Here is the full wrapper class I've created for handling the grid :
import Foundation
import GiphyUISDK
class GiphyController: NSObject {
//var giphy: GiphyViewController?
var gridController: GiphyGridController?
var mainView: UIView?
var content: GPHContent?
var querySearch: String?
var currentType: String?
var channel: FlutterMethodChannel?
var rootController: UIViewController?
// Called inside AppDelegate
func initVC(_ ch: FlutterMethodChannel, _ rootVC: UIViewController) {
mainView = UIView()
channel = ch
//setCacheSize(size: 100)
// TEST WITH VIEW CONTROLLER
//
// rootController = rootVC
// giphy = GiphyViewController()
// giphy!.mediaTypeConfig = [.recents, .stickers, .emoji, .text, .gifs]
// giphy!.theme = GPHTheme(type: Constants.onDarkMode ? .darkBlur : .lightBlur)
// giphy!.stickerColumnCount = GPHStickerColumnCount.three
// giphy!.shouldLocalizeSearch = true
// rootController?.present(giphy!, animated: true, completion: nil)
// return
}
// Called only once when the modal show up
func initController(completion: ((Bool?) -> Void?)) {
if(gridController == nil) {
gridController = GiphyGridController()
gridController!.delegate = self
// space between cells
gridController!.cellPadding = 4.0
// the scroll direction of the grid
gridController!.direction = .vertical
// the number of "tracks" is the span count. it represents num columns for vertical grids & num rows for horizontal grids
gridController!.numberOfTracks = 3
// hide the checkered background for stickers if you'd like (true by default)
//gridController!.showCheckeredBackground = false
gridController!.view.backgroundColor = .clear
gridController!.theme = GiphyGridTheme()
// by default, the waterfall layout sizes cells according to the aspect ratio of the media
// the fixedSizeCells setting makes it so each cell is square
// this setting only applies to Stickers (not GIFs)
gridController!.fixedSizeCells = true
mainView!.addSubview(gridController!.view)
gridController!.view.translatesAutoresizingMaskIntoConstraints = false
gridController!.view.leftAnchor.constraint(equalTo: mainView!.safeLeftAnchor).isActive = true
gridController!.view.rightAnchor.constraint(equalTo: mainView!.safeRightAnchor).isActive = true
gridController!.view.topAnchor.constraint(equalTo: mainView!.safeTopAnchor).isActive = true
gridController!.view.bottomAnchor.constraint(equalTo: mainView!.safeBottomAnchor).isActive = true
completion(true)
}
else {
completion(false)
}
}
// Called when the modal is dismissed
func dismiss(completion: ((Bool?)->Void?)) {
if(gridController != nil) {
gridController!.view.removeFromSuperview()
}
gridController = nil
querySearch = nil
completion(true)
}
func getMedia(id: String, completion: ((String?) -> Void)? = nil) {
GiphyCore.shared.gifByID(id) { (response, error) in
if let media = response?.data {
DispatchQueue.main.sync { [weak self] in
let url = media.url(rendition: .fixedWidth, fileType: .gif)
completion?(url)
}
}
else {
completion?(nil)
}
}
}
func downloadMedia(id: String, completion: ((Data?) -> Void)? = nil) {
getMedia(id: id, completion: { url in
if(url != nil) {
GPHCache.shared.downloadAssetData(url!) { (data, error) in
completion?(data)
}
}
})
}
func search(query: String, type: String, completion: ((Bool?) ->Void?)) {
if(gridController != nil) {
querySearch = query
let type = getTypeWithString(type: type)
// TODO : change the localization
gridController!.content = GPHContent.search(withQuery: query, mediaType: type, language: .french, includeDynamicResults: true)
gridController!.update()
completion(true)
}
else {
completion(false)
}
}
func clearSearch(completion: ((Bool?)->Void?)) {
querySearch = nil
if(currentType != nil) {
update(type: currentType!, completion: { success in
completion(success)
})
}
else {
completion(false)
}
}
// Called to update the grid content according the current type
func update(type: String, completion: ((Bool?)->Void?)) {
if(gridController == nil) {
completion(false)
return
}
// Only affected here in order to reset the current TAB in grid after clearSearch()
currentType = type
// If there is already a keyword in search we just update the content according the query
// Otherwise moving the content the trend according type
//
// If the type is recent or emoji there is no query to make for because this is fixed result
if(querySearch != nil && type != "recently" && type != "emoji") {
search(query: querySearch!, type: type,completion: { success in
completion(success)
})
return
}
// Update the grid gif according the type
var content = GPHContent.recents
switch type {
case "recently":
// initialize with recents so don't need to re affect
break
case "stickers":
content = GPHContent.trending(mediaType: .sticker)
break
case "emoji":
content = GPHContent.emoji
break
case "text":
content = GPHContent.trending(mediaType: .text)
break
case "gif":
content = GPHContent.trending(mediaType: .gif)
break
default:
break
}
gridController!.content = content
gridController!.update()
completion(true)
}
func getRecentCount(completion: ((Int?)->Void?)) {
let count = GPHRecents.count
completion(count)
}
func clearRecent(completion: ((Bool?) -> Void)) {
GPHRecents.clear()
completion(true)
}
func getTypeWithString(type: String) -> GPHMediaType {
// The search is not possible when this is recent or emoji because this is fixed list
// for the user
switch type {
case "recently":
return .sticker
case "text":
return .text
case "stickers":
return .sticker
case "emoji":
return .sticker
case "gif":
return .gif
default:
return .sticker
}
}
func setCacheSize(size: Int) {
GPHCache.shared.cache.diskCapacity = size
GPHCache.shared.cache.memoryCapacity = size
}
func clearCache() {
GPHCache.shared.clear()
}
}
extension GiphyController: GPHGridDelegate {
func contentDidUpdate(resultCount: Int, error: Error?) {
}
func didSelectMoreByYou(query: String) {
}
func didScroll(offset: CGFloat) {
channel?.invokeMethod("onScroll", arguments: offset)
}
func contentDidUpdate(resultCount: Int) {
}
func didSelectMedia(media: GPHMedia, cell: UICollectionViewCell) {
var args = [String: Any]()
args["id"] = media.id
args["ratio"] = media.aspectRatio
channel?.invokeMethod("onMediaPicked", arguments: args)
}
}
class GiphyGridTheme: GPHTheme {
override var backgroundColorForLoadingCells: UIColor {
return .clear
}
}