Giphy/giphy-ios-sdk

[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

      }

}