ios - Processing image collage for UITableViewCells – Method that would scale for hundreds of cells? -
problem: using art assets music accessed via apple apis (all local) create 4 image collage image in cell playlist in table view. have tried few methods go on performance acceptable level, there kind of hiccup.
method 1) ran processing code return collaged image each cell. fine few playlists had on device, got reports heavy users couldn't scroll.
method 2) in viewdidload(), iterated on playlists , saved collaged image , 1 of uuids in mutable array fetched image cells using uuid. seems work ok delay on loading view - reason subsequent loads taking longer time.
so before try troubleshoot method 2 more, wondering if straight going wrong way? have looked in gcd , nscache, , far gcd concerned don't know enough make proper design pattern exploit it, if possible since things ui updates , storage access might whats getting in way.
import uikit import mediaplayer class playlists: uitableviewcontroller, uisearchbardelegate, uisearchcontrollerdelegate { ... var compositedcellimages:[(uiimage, uint64)] = [] ... override func viewdidload() { super.viewdidload() let cloudfilter:mpmediapropertypredicate = mpmediapropertypredicate(value: false, forproperty: mpmediaitempropertyisclouditem, comparisontype: mpmediapredicatecomparison.equalto) playlistsquery.addfilterpredicate(cloudfilter) playlistquerycollections = playlistsquery.collections?.filter{$0.value(forproperty: mpmediaplaylistpropertyname) as? string != "purchased"} nsarray? var tmparray:[mpmediaplaylist] = [] playlists = playlistquerycollections as! [mpmediaplaylist] playlist in playlists { if playlist.value(forproperty: "parentpersistentid") as! nsnumber! == playlistfolderid { tmparray.append(playlist) compositedcellimages.append(playlistlistimage(inputplaylistid: playlist.persistentid)) } } playlists = tmparray ... } // mark: - table view data source ... override func tableview(_ tableview: uitableview, cellforrowat indexpath: indexpath) -> uitableviewcell { let cell = self.tableview.dequeuereusablecell(withidentifier: "playlistcell", for: indexpath) as! playlistcell ... let currentitem = playlists[indexpath.row] ... cell.playlistcellimage.image = compositedcellimages[indexpath.row].0 } } return cell } ... func playlistlistimage(inputplaylistid:mpmediaentitypersistentid) -> (uiimage,uint64) { var playlistdata:[mpmediaitem] = [] var pickedartwork:[uiimage] = [] var shuffledindexes:[int] = [] let playlistdetailimage:collageimageview = collageimageview() playlistdetailimage.frame = cgrect(x: 0, y: 0, width: 128, height: 128) let playlistdatapredicate = mpmediapropertypredicate(value: nsnumber(value: inputplaylistid uint64), forproperty: mpmediaplaylistpropertypersistentid, comparisontype:mpmediapredicatecomparison.equalto) let playlistdataquery = mpmediaquery.playlists() let cloudfilter:mpmediapropertypredicate = mpmediapropertypredicate(value: false, forproperty: mpmediaitempropertyisclouditem, comparisontype: mpmediapredicatecomparison.equalto) playlistdataquery.addfilterpredicate(cloudfilter) playlistdataquery.addfilterpredicate(playlistdatapredicate) playlistdata = playlistdataquery.items! playlistdata = playlistdata.filter{$0.mediatype == mpmediatype.music} (index,_) in playlistdata.enumerated() { shuffledindexes.append(index) } shuffledindexes.shuffleinplace() (_,element) in shuffledindexes.enumerated() { if playlistdata[element].artwork != nil { pickedartwork.append(playlistdata[element].artwork!.image(at: cgsize(width: 64, height: 64))!) } if pickedartwork.count == 4 { break } } while pickedartwork.count < 4 { if pickedartwork.count == 0 { pickedartwork.append(uiimage(named: "missing")!) } else { pickedartwork.shuffleinplace() pickedartwork.append(pickedartwork[0]) } } pickedartwork.shuffleinplace() playlistdetailimage.drawincontext(pickedartwork, matrixsize: 2) return ((playlistdetailimage.image)!,inputplaylistid) } ... } ... class collageimageview: uiimageview { var inputimages:[uiimage] = [] var rows:int = 1 var cols:int = 1 func drawincontext(_ imageset: [uiimage], matrixsize: int) { let frameleg:int = int(self.frame.width/cgfloat(matrixsize)) var increment:int = 0 uigraphicsbeginimagecontextwithoptions(self.frame.size, false, uiscreen.main.scale) self.image?.draw(in: self.frame) col in 1...matrixsize { row in 1...matrixsize { imageset[increment].draw(in: cgrect(x: (row - 1) * frameleg, y: (col-1) * frameleg, width: frameleg, height: frameleg)) increment += 1 } } self.image = uigraphicsgetimagefromcurrentimagecontext() uigraphicsendimagecontext() } }
one approach modify original method 1 add lazy loading , caching.
for example if how lazy loading see https://developer.apple.com/library/content/samplecode/lazytableimages/introduction/intro.html
the basic idea attempt compute image when scrolling has completed (in above example loading image url, replace calculation).
in addition, can cache results of calculation each row, user scrolls , forth, can check cached values first. clear cache in didreceivememorywarning.
so in tableview(_: uitableview, cellforrowat: indexpath)
if <cache contains image row> { cell.playlistcellimage.image = <cached image> } else if tableview.isdragging && !tableview.isdecelerating { let image = <calculate image row> <add image cache> cell.playlistcellimage.image = image } else { cell.playlistcellimage.image = <placeholder image> }
then override delegate methods scroll view
override func scrollviewdidenddecelerating(_ scrollview: uiscrollview) { loadimagesforonscreenrows() } override func scrollviewdidenddragging(_ scrollview: uiscrollview, willdecelerate decelerate: bool) { loadimagesforonscreenrows() }
and implement loadimagesforonscreenrows()
for path in tableview.indexpathsforvisiblerows { let image = <calculate image row> <add image cache> if let cell = tableview.cellforrow(at: path) { cell.playlistcellimage.image = image } }
one last optimisation push actual calculation onto background thread should find lazy loading above enough.
update - compute on background thread.
general idea run calculation using dispatchqueue on background queue, when results ready, update display on ui thread.
something following (untested) code
func loadimageinbackground(inputplaylistid:mpmediaentitypersistentid, completion:@escaping (uiimage, uint64)) { let backgroundqueue = dispatchqueue.global(dos: dispatchqos.qosclass.background) backgroundqueue.async { let (image,n) = playlistlistimage(inputplaylistid:inputplaylistid) dispatchqueue.main.async { completion(image,n) } } }
and in tableview(_: uitableview, cellforrowat: indexpath) instead of computing image directly, call background method:
if <cache contains image row> { cell.playlistcellimage.image = <cached image> } else if tableview.isdragging && !tableview.isdecelerating { cell.playlistcellimage.image = <placeholder image> loadimageinbackground(...) { (image, n) in if let cell = tableview.cellforrow(at:indexpath) { cell.playlistcellimage.image = image } <add image cache> } } else { cell.playlistcellimage.image = <placeholder image> }
and similar update in loadimagesforonscreenrows().
notice code retrieve cell again within callback handler. because update can occur asynchronously, quite possible original cell have been reused, need make sure updating correct cell
Comments
Post a Comment