How to Stop UI thread until Network Thread Completes? - ios

I am displaying some data from JSON into my AccordionView. The problem is that my View thread completes executing before my Network Thread. So I Can't populate the Data which I get from the Array to my AccordionView.
So it shows that Array index out of range exception. I also tried using the reloadData Method But it is of no use.
I researched this issue and found about GCD. I can't figure out how to use GCD for this issue.
This is the AccordionView Library.
import UIKit
import Alamofire
import SwiftyJSON
var CourseIdinController : String!
var CourseDescriptionC: String!
var ChapNameC : [String] = []
var titleLabel : UILabel?
class CoursePageController: UIViewController {
#IBOutlet weak var courseDesc: UITextView!
var CourseName : String!
var ChapName : [String] = []
var LessonName : [String] = []
var myAccordionView : MKAccordionView?
override func viewDidLoad() {
super.viewDidLoad()
self.title = CourseName
self.courseDesc.text = CourseDescriptionC
self.courseDesc.setContentOffset(CGPointZero, animated: true)
view.bounds.size.height = 450.0
myAccordionView = MKAccordionView(frame: CGRectMake(0, 111, CGRectGetWidth(view.bounds), CGRectGetHeight(view.bounds)));
println(ChapNameC.count)
myAccordionView!.delegate = self;
myAccordionView!.dataSource = self;
view.addSubview(myAccordionView!);
getData()
getlData()
}
func getData(){
Alamofire.request(.GET, "http://www.wgve.com/index.php/capp/get_chapter_by_course_id/\(CourseIdinController)")
.responseJSON { (_, _, data, _) in
let json = JSON(data!)
let catCount = json.count
for index in 0...catCount-1 {
let cname = json[index]["CHAPTER_NAME"].string
self.ChapName.append(cname!)
println(self.ChapName[index])
}
self.myAccordionView!.tableView?.reloadData()
}}
func getlData(){
Alamofire.request(.GET, "http://www.wgve.com/index.php/capp/get_lesson_by_course_id/\(CourseIdinController)")
.responseJSON { (_, _, data, _) in
let json = JSON(data!)
let catCount = json.count
for index in 0...catCount-1 {
let cname = json[index]["LESSON_NAME"].string
self.LessonName.append(cname!)
}
self.myAccordionView!.tableView?.reloadData()
}}
func accordionView(accordionView: MKAccordionView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell : UITableViewCell? = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: nil)
//cell?.imageView = UIImageView(image: UIImage(named: "lightGrayBarWithBluestripe"))
// Background view
var bgView : UIView? = UIView(frame: CGRectMake(0, 0, CGRectGetWidth(accordionView.bounds), 50))
var bgImageView : UIImageView! = UIImageView(image: UIImage(named: "lightGrayBarWithBluestripe"))
bgImageView.frame = (bgView?.bounds)!
bgImageView.contentMode = UIViewContentMode.ScaleToFill
bgView?.addSubview(bgImageView)
cell?.backgroundView = bgView
// You can assign cell.selectedBackgroundView also for selected mode
cell?.textLabel?.text = LessonName[0]
return cell!
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
// MARK: - Implemention of MKAccordionViewDatasource method
extension CoursePageController : MKAccordionViewDatasource {
func numberOfSectionsInAccordionView(accordionView: MKAccordionView) -> Int {
println(ChapName.count)
return ChapName.count
}
func accordionView(accordionView: MKAccordionView, numberOfRowsInSection section: Int) -> Int {
println(LessonName.count)
return LessonName.count
}
}
// MARK: - Implemention of MKAccordionViewDatasource method
extension CoursePageController : MKAccordionViewDatasource {
func numberOfSectionsInAccordionView(accordionView: MKAccordionView) -> Int {
return ChapName.count
}
func accordionView(accordionView: MKAccordionView, numberOfRowsInSection section: Int) -> Int {
return LessonName.count
}
}
// MARK: - Implemention of MKAccordionViewDelegate method
extension CoursePageController : MKAccordionViewDelegate {
func accordionView(accordionView: MKAccordionView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 50
}
func accordionView(accordionView: MKAccordionView, heightForHeaderInSection section: Int) -> CGFloat {
return 50
}
func accordionView(accordionView: MKAccordionView, viewForHeaderInSection section: Int, isSectionOpen sectionOpen: Bool) -> UIView? {
var view : UIView! = UIView(frame: CGRectMake(0, 0, CGRectGetWidth(accordionView.bounds), 50))
// Background Image
var bgImageView : UIImageView = UIImageView(frame: view.bounds)
bgImageView.image = UIImage(named: ( sectionOpen ? "grayBarSelected" : "grayBar"))!
view.addSubview(bgImageView)
// Arrow Image
var arrowImageView : UIImageView = UIImageView(frame: CGRectMake(15, 15, 20, 20))
arrowImageView.image = UIImage(named: ( sectionOpen ? "close" : "open"))!
view.addSubview(arrowImageView)
// Title Label
titleLabel = UILabel(frame: CGRectMake(50, 0, CGRectGetWidth(view.bounds) - 120, CGRectGetHeight(view.bounds)))
if (ChapName.count != 0) {
let count = ChapName.count
for index in 0...count-1 {
titleLabel!.text = self.ChapName[index]
}
}
titleLabel!.textColor = UIColor.whiteColor()
view.addSubview(titleLabel!)
return view
}
}
Note:
It shows me error because the ChapName & lesson name array are Empty. If i Hardcode the values. It prints both the array's in my console.
May be the accordionView doesn't support the reload Method?
or Where am I Going Wrong ?
UPDATE
numberOfSectionsInAccordionView throws Error like this fatal error: Can't form Range with end < start which on line 233
Refer this link
2.The titleLabel returns only the last item in the Array. I don't know Why ?

You should make sure all your json data is successfully stored into an array "accordionDataArray" before you attempt to reload it.
ATTENTION : make sure that the datasource methods numberOfRowsInSection and numberOfSectionsInAccordionView rely on the accordionDataArray content (or whatever Array you have) to determine the number of sections and rows of the accordion.
P.S. Anyway Why would you reload the content of the accordion in a loop?
Do this instead:
for index in 0...catCount-1 {
let cname = json[index]["CHAPTER_NAME"].string
self.ChapName.append(cname!)
println(self.ChapName[index])
}
self.myAccordionView!.tableView?.reloadData()
UPDATE:
To get passed the fact that ChapName & lessonName array are asked for some value when they are Empty , you need to check that like this whenever they occur:
if (ChapName.count != 0) {
titleLabel!.text = self.ChapName[0]
}
if (LessonName.count != 0){
// do something
}
Make sure you update your code and test. In the method:
func accordionView(accordionView: MKAccordionView, viewForHeaderInSection section: Int, isSectionOpen sectionOpen: Bool) -> UIView? {
[...]
if (ChapName.count != 0) {
titleLabel!.text = self.ChapName[0]
}
[...]
}
UPDATE 2: The library you are using doesn't seem to allow the number of sections to be 0. I suggest you contact the library's owner if you can't make sure that the number of section is at least 1.
Regarding to your other issue with titleLabel being always the last item of the array you need to change this:
if (ChapName.count != 0) {
let count = ChapName.count
for index in 0...count-1 {
titleLabel!.text = self.ChapName[index]
}
}
into this:
titleLabel!.text = self.ChapName[section]

About why the code above does not work:
numberOfRowsInSection returns a hardcoded value, regardless of the JSON response.
About stopping the UI thread:
We are somewhat glad you can't do that. GCD helps you to share processor time efficiently, the exact opposite of holding the UI thread hostage. Image the consequence if somehow your network thread would hang. What would happen to the UI thread? Don't do it.
Solutions: Your make your UI thread clever enough to handle absence of datum. i.e. do not return hardcoded counts for sections or cells. When you receive data, update your records (the model), send a refresh message to the UI (such as reloadData in UITableView).

Related

Issue with Activity Indicator While loading data in UITableView

I'm getting record from server using php
As i'm getting 11 record currently so i want in starting i will show just 6 records and remaining next 5 record will show when user reached at last cell while scrolling. So this process is in working form, but the problem is while running, it working so fast that before reaching last row all records are already showing while scrolling and the activity indicator is just animating at the bottom of tableView.
I don't know what is the problem.
Also i want when user reached at last cell, activity indicator start animating while loading data .
Here is my code
import UIKit
import AlamofireImage
struct property{
let property_Id : String
let propertyTitle : String
let imageURL : String
let user_Id : String
let area : String
let bed : String
let unit : String
let bath : String
let price : String
}
class searchRecordsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource,favouriteButtonTableViewCellDelegate {
var y = 6
var m = 12
#IBOutlet weak var tableView : UITableView!
var RESULT : [NSDictionary] = []
var myProperty = [property]()
var myPropertyCopy = [property]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
//MARK: Getting dictionary data from previous controller
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationItem.title = "Results"
self.navigationController?.navigationBar.tintColor = UIColor.black
//MARK: Getting dictionary data from previous controller
for item in RESULT {
let propertyInfo = property(property_Id: String(item["propertyId"]! as! Int), propertyTitle: item["propertyTitle"]! as! String, imageURL: item["imagePath"]! as! String, user_Id: String(item["userId"]! as! Int), area: item["area"]! as! String, bed: item["bed"]! as! String, unit: item["unit"]! as! String, bath: item["bath"]! as! String, price: item["price"]! as! String )
myProperty.append(propertyInfo)
}
//MARK: Inserting first 6 records in Array
for i in 0 ..< 6 {
if !(myProperty.indices.contains(i)) {
break
}
myPropertyCopy.append(myProperty[i])
}
}
func downloadImage(imagePath : String, theIMAGEVIEW : UIImageView) {
let myUrl = URL(string: URL_IP+imagePath);
//MARK: AlamofireImage to download the image
theIMAGEVIEW.af_setImage(withURL: myUrl!, placeholderImage: #imageLiteral(resourceName: "addProperty"), filter: nil, progress: nil, runImageTransitionIfCached: true, completion: nil)
}
func tableView(_ tableView:UITableView, numberOfRowsInSection section:Int) -> Int
{
return myPropertyCopy.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "RecordCell") as! searchrecordsTableViewCell
let property = myPropertyCopy[indexPath.row]
cell.area.text = property.area+" "+property.unit
if property.bath == ""{
cell.bath.text = property.bath
}
else{
cell.bath.text = property.bath+" Baths"
}
if property.bed == ""{
cell.bed.text = property.bed
}
else{
cell.bed.text = property.bed+" Baths"
}
cell.propertyTitle.text = property.propertyTitle
cell.price.text = convertAMOUNT(price : property.price)
downloadImage(imagePath: property.imageURL, theIMAGEVIEW: cell.myImageView)
//----
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if myPropertyCopy.count != myProperty.count{
let lastRow = myPropertyCopy.count - 1
if indexPath.row == lastRow {
let spinner = UIActivityIndicatorView(activityIndicatorStyle: .gray)
spinner.startAnimating()
spinner.frame = CGRect(x: CGFloat(0), y: CGFloat(0), width: tableView.bounds.width, height: CGFloat(44))
self.tableView.tableFooterView = spinner
self.tableView.tableFooterView?.isHidden = false
moreData()
}
}
}
func moreData(){
for i in y ..< m {
if !(myProperty.indices.contains(i)) {
break
}
myPropertyCopy.append(myProperty[i])
}
y = y + 10
m = m + 10
self.tableView.reloadData()
}
}
currently my TableView looks like
See output here
Thanks in Advance.
After a long practice i have done my problem, i have just added UIScrollView delegate function for checking if the user reached at last cell then start activityIndicator for 3 seconds and then load data and that's it.
As i have just very small amount of data that's why i'm start ing UIactivityIndicator for 3 seconds before getting data.
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// UITableView only moves in one direction, y axis
let currentOffset = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
if maximumOffset - currentOffset <= 10.0 {
let spinner = UIActivityIndicatorView(activityIndicatorStyle: .gray)
if myPropertyCopy.count != myProperty.count {
//print("this is the last cell")
spinner.startAnimating()
spinner.frame = CGRect(x: CGFloat(0), y: CGFloat(0), width: tableView.bounds.width, height: CGFloat(44))
spinner.hidesWhenStopped = true
self.tableView.tableFooterView = spinner
self.tableView.tableFooterView?.isHidden = false
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
//MARK: Loading more data
self.moreData()
}
}
else{
self.tableView.tableFooterView?.isHidden = true
spinner.stopAnimating()
}
}
}

Initial scroll lag in UITableView cells with resized large images?

I'm basically having the same issue as this question:
Slow scroll on UITableView images
My UITableView contains a UIImageView that is 87x123 size.
When my UITableViewController is loaded, it first calls a function that loops through an array of images. These images are high resolutions stored from the photo library. In each iteration, it retrieves the image and resize each image down to 87x123, then stores it back into the original image in the array.
When all the images has been resized and stored, it calls self.tableView.reloadData to populate the data in the array into the cells.
However, like the mentioned question, my UITablView is choppy and lags if I scroll fast before all the images has been resized and stored in the array.
Here's the problematic code:
extension UIImage
{
func resizeImage(originalImage: UIImage, scaledTo size: CGSize) -> UIImage
{
// Avoid redundant drawing
if originalImage.size.equalTo(size)
{
return originalImage
}
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
originalImage.draw(in: CGRect(x: CGFloat(0.0), y: CGFloat(0.0), width: CGFloat(size.width), height: CGFloat(size.height)))
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
func loadImages()
{
DispatchQueue.global(qos: .background).async {
for index in 0..<self.myArray.count
{
if let image = self.myArray[index].image
{
self.myArray[index].image = image.resizeImage(originalImage: image, scaledTo: CGSize(width: 87, height: 123) )
}
if index == self.myArray.count - 1
{
print("FINISHED RESIZING ALL IMAGES")
}
}
}
tableView.reloadData()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
...
// Size is 87x123
let thumbnailImage = cell.viewWithTag(1) as! UIImageView
DispatchQueue.main.async{
thumbnailImage.image = self.myArray[indexPath.row]
}
thumbnailImage.contentMode = UIViewContentMode.scaleAspectFill
thumbnailImage.layer.borderColor = UIColor.black.cgColor
thumbnailImage.layer.borderWidth = 1.0
thumbnailImage.clipsToBounds = true
return cell
}
I know to do any Non-UI operations in the background thread, which is what I do. Then all I do in cellForRowAt is load the image into the cell using its indexPath.row.
The problem is, as previously mentioned, if I start to scroll the UITableView BEFORE FINISHED RESIZING ALL IMAGES is printed out, i.e. before all the images has been resized, there is noticeable lag and slowness.
However, if I wait UNTIL all the images has been resized and FINISHED RESIZING ALL IMAGES is called before scrolling the UITableView, the scrolling is smooth without any lags.
I can put a loading indicator and have the user wait until all images has been resized and loaded into the cells before having user interaction, but that would be an annoyance since it takes about 8 seconds to resize all the high-res images (18 images to resize).
Is there a better way I can fix this lag?
UPDATE: Following #iWheelBuy's second example, I've implemented the following:
final class ResizeOperation: Operation {
private(set) var image: UIImage
let index: Int
let size: CGSize
init(image: UIImage, index: Int, size: CGSize) {
self.image = image
self.index = index
self.size = size
super.init()
}
override func main() {
image = image.resizeImage(originalImage: image, scaledTo: size)
}
}
class MyTableViewController: UITableViewController
{
...
lazy var resizeQueue: OperationQueue = self.getQueue()
var myArray: [Information] = []
internal struct Information
{
var title: String?
var image: UIImage?
init()
{
}
}
override func viewDidLoad()
{
....
loadImages()
...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
...
// Size is 87x123
let thumbnailImage = cell.viewWithTag(1) as! UIImageView
DispatchQueue.main.async{
thumbnailImage.image = self.myArray[indexPath.row].image
}
thumbnailImage.contentMode = UIViewContentMode.scaleAspectFill
return cell
}
func loadImages()
{
let size = CGSize(width: 87, height: 123)
for item in myArray
{
let operations = self.myArray.enumerated().map({ ResizeOperation(image: item.image!, index: $0.offset, size: size) })
operations.forEach { [weak queue = resizeQueue, weak controller = self] (operation) in
operation.completionBlock = { [operation] in
DispatchQueue.main.async { [image = operation.image, index = operation.index] in
self.update(image: image, index: index)
}
}
queue?.addOperation(operation)
}
}
}
func update(image: UIImage, index: Int)
{
myArray[index].image = image
tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: UITableViewRowAnimation.fade)
}
}
However, upon calling tableView.reloadRows, I receive a crash with the error:
attempt to delete row 0 from section 0, but there are only 0 sections
before the update
I'm a bit confused on what it means and how to resolve it.
It is hard to determine the reason why you have lags. But there are some thoughts that might help you to make you code more performant.
Try using some small images at start and see, if it is the size of original image that influences bad performance. Also try to hide these lines and see if anything changes:
// thumbnailImage.contentMode = UIViewContentMode.scaleAspectFill
// thumbnailImage.layer.borderColor = UIColor.black.cgColor
// thumbnailImage.layer.borderWidth = 1.0
// thumbnailImage.clipsToBounds = true
Your code is a little bit oldschool, for loop in loadImages can become more readable with just a few lines of code by applying map or forEach to your image array.
Also, about your array of images. You read it from main thread and modify it from background thread. And you do it simultaneously. I'd suggest to make only image resizing on background... unless you are sure there will be no bad consequences
Check the code example #1 below how you current code can look like.
On the other hand, you can go some other way. For example you can set some placeholder image at start and update cells when image for some specific cell is ready. Not all images at once! If you go with some serial queue, you will get image updates every 0.5 seconds and the UI updates will be fine.
Check the code example #2. It wasn't tested, just to show the way you can go.
Btw, have you tried changing QualityOfService from background to userInitiated? It might decrease resizing time... or not (:
DispatchQueue.global(qos: .userInitiated).async {
// code
}
Example #1
extension UIImage {
func resize(to size: CGSize) -> UIImage {
guard self.size.equalTo(size) else { return self }
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
draw(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
static func resize(images: [UIImage], size: CGSize, completion: #escaping ([UIImage]) -> Void) {
DispatchQueue.global(qos: .background).async {
let newArray = images.map({ $0.resize(to: size) })
DispatchQueue.main.async {
completion(newArray)
}
}
}
}
final class YourController: UITableViewController {
var myArray: [UIImage] = []
override func viewDidLoad() {
super.viewDidLoad()
self.loadImages()
}
}
fileprivate extension YourController {
func loadImages() {
UIImage.resize(images: myArray, size: CGSize(width: 87, height: 123)) { [weak controller = self] (newArray) in
guard let controller = controller else { return }
controller.myArray = newArray
controller.tableView.reloadData()
}
}
}
Example #2
final class ResizeOperation: Operation {
private(set) var image: UIImage
let index: Int
let size: CGSize
init(image: UIImage, index: Int, size: CGSize) {
self.image = image
self.index = index
self.size = size
super.init()
}
override func main() {
image = image.resize(to: size)
}
}
final class YourController: UITableViewController {
var myArray: [UIImage] = []
lazy var resizeQueue: OperationQueue = self.getQueue()
override func viewDidLoad() {
super.viewDidLoad()
self.loadImages()
}
private func getQueue() -> OperationQueue {
let queue = OperationQueue()
queue.qualityOfService = .background
queue.maxConcurrentOperationCount = 1
return queue
}
}
fileprivate extension YourController {
func loadImages() {
let size = CGSize(width: 87, height: 123)
let operations = myArray.enumerated().map({ ResizeOperation(image: $0.element, index: $0.offset, size: size) })
operations.forEach { [weak queue = resizeQueue, weak controller = self] (operation) in
operation.completionBlock = { [operation] in
DispatchQueue.main.async { [image = operation.image, index = operation.index] in
controller?.update(image: image, index: index)
}
}
queue?.addOperation(operation)
}
}
func update(image: UIImage, index: Int) {
myArray[index] = image
tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: UITableViewRowAnimation.fade)
}
}

UIImage created with animatedImageWithImages have bugs?

Recently I found sometimes my gif image not shown. So I write a test code for it:
import UIKit
import ImageIO
extension UIImage {
static func gifImageArray(data: NSData) -> [UIImage] {
let source = CGImageSourceCreateWithData(data, nil)!
let count = CGImageSourceGetCount(source)
if count <= 1 {
return [UIImage(data: data)!]
} else {
var images = [UIImage](); images.reserveCapacity(count)
for i in 0..<count {
let image = CGImageSourceCreateImageAtIndex(source, i, nil)!
images.append( UIImage(CGImage: image) )
}
return images;
}
}
static func gifImage(data: NSData) -> UIImage? {
let gif = gifImageArray(data)
if gif.count <= 1 {
return gif[0]
} else {
return UIImage.animatedImageWithImages(gif, duration: NSTimeInterval(gif.count) * 0.1)
}
}
}
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let _gifData : NSData = NSData(contentsOfURL: NSURL(string: "https://upload.wikimedia.org/wikipedia/commons/d/d3/Newtons_cradle_animation_book_2.gif")!)!
lazy var _gifImageArray : [UIImage] = UIImage.gifImageArray(self._gifData)
lazy var _gifImage : UIImage = UIImage.gifImage(self._gifData)!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let table = UITableView(frame: self.view.bounds, style: .Plain)
table.dataSource = self
table.delegate = self
self.view.addSubview(table)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1000;
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let identifier = "cell"
var cell : UITableViewCell! = tableView.dequeueReusableCellWithIdentifier(identifier);
if cell == nil {
cell = UITableViewCell(style: .Default, reuseIdentifier: identifier)
}
if let imageView = cell.imageView {
/** 1. use the same UIImage object created by animatedImageWithImages
the image disappear when reuse, and when touching cell, it reappear. */
imageView.image = self._gifImage
/** 2. create different UIImage by using same image array. this still not work */
// imageView.image = UIImage.animatedImageWithImages(self._gifImageArray, duration: NSTimeInterval(self._gifImageArray.count) * 0.1)
/** 3. create different UIImage from data, this seems work
but use a lot of memories. and seems it has performance issue. */
// imageView.image = UIImage.gifImage(self._gifData)
/** 4. use same image array as imageView's animationImages.
this kind works, but have different api than just set image.
and when click on cell, the animation stops. */
// imageView.image = self._gifImageArray[0]
// imageView.animationImages = self._gifImageArray
// imageView.startAnimating()
}
return cell
}
}
Notice in the cellForRowAtIndexPath: method, I tried different way to set imageView.
when I po object in lldb, I notice the animated imageView have a CAKeyframeAnimation. does this makes a tip?
How can I make the gif shown perfectly? Any suggestion would be helpful.
Here is a preview: after scroll, gif disappear, and reappear when touch
After I tried many times, I have finally figured out that I need to change:
imageView.image = self._gifImage
to:
imageView.image = nil // this is the magical!
imageView.image = self._gifImage
just set image to nil before set to new image, and the animation work!! It's just like a magical!
This is definitely apple's bug.
try by replacing imageView with myImageview or other name because cell have default imageView property so when you call cell.imageView then it returns cell's default imageview i think. so change your cell's imageview's variable name. hope this will help :)
maybe image class need SDAnimatedImage instead of UIImage

How Do I Initialize Two Instances of NSObject in the same ViewController - Swift

Please bear with me. I'm new to programming and new to StackOverflow. I hope that my question will grant me a warm response for entering the programming community. An acquaintance, whose opinion I trust, told me to post this email to him on StackOverflow.
What do I have to do to get two instances of NSObject to work in the same ViewController? I've initialized an NSObject subclass called SideBar and RightSideBar. They both draw from NSObject. The cells in the menu are called created by a TableViewController I created programatically. I followed a tutorial that did everything programmatically because I didn't know that Storyboard is better for building things.
Below is the code base.
EDITED: Sorry for being long winded. I don't know how to make this any shorter and as complete
===========
****Note the SideBar subclass is the left menu. The RightSideBar class has the same initializer setup and is the right menu. I want to be able to make them both appear on the same ViewController in the same instance of the same ViewController if possible.****
This is the left TableViewController:
import UIKit
//this protocol of the sidebar delegate manages the selection of the item in the row.
protocol SidebarTableViewControllerDelegate {
func sidebarControlDidSelectRow(indexPath: NSIndexPath)
}
class SidebarTableViewController: UITableViewController {
//setting up the delegate and array of menu items.
var delegate:SidebarTableViewControllerDelegate?
var tableData:Array <String> = []
var imageData:[UIImage] = []
// MARK: - Table view data source
//Setting up the number of sections in the menu
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
//Setting up the number of items in the menu
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
//Setting up the menu look for the main screen after login.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("Cell") as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
//configure the cell...
cell!.backgroundColor = UIColor.clearColor()
cell!.textLabel?.textColor = UIColor.darkTextColor()
let selectedView:UIView = UIView(frame: CGRect(x: 0, y: 0, width: cell!.frame.size.width, height: cell!.frame.size.height))
selectedView.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.3)
cell!.selectedBackgroundView = selectedView
}
cell!.textLabel!.text = tableData[indexPath.row]
cell!.imageView!.image = imageData[indexPath.row]
return cell!
}
//Setting up the height for each cell of the table
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 45.0
}
//Setting up the selection of the item in the cell.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
delegate?.sidebarControlDidSelectRow(indexPath)
}
override func viewDidLoad() {
}
override func didReceiveMemoryWarning() {
}
}
This is the right table view controller:
//setting up the RightSidebarControllerDelegate
protocol RightSidebarTableViewControllerDelegate {
func rightSidebarControlDidSelectRow(indexPath: NSIndexPath)
}
class RightSidebarTableViewController: UITableViewController {
//setting up the delegate and array of menu items.
var delegate:RightSidebarTableViewControllerDelegate?
var rightTableData:Array <String> = []
var rightImageData:[UIImage] = []
// MARK: - Table view data source
//Setting up the number of sections in the menu
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
//Setting up the number of items in the menu
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rightTableData.count
}
//Setting up the menu look for the main screen after login.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("Cell") as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
//configure the cell...
cell!.backgroundColor = UIColor.clearColor()
cell!.textLabel?.textColor = UIColor.darkTextColor()
let selectedView:UIView = UIView(frame: CGRect(x: 0, y: 0, width: cell!.frame.size.width, height: cell!.frame.size.height))
selectedView.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.3)
cell!.selectedBackgroundView = selectedView
}
cell!.textLabel!.text = rightTableData[indexPath.row]
cell!.imageView!.image = rightImageData[indexPath.row]
return cell!
}
//Setting up the height for each cell of the table
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 45.0
}
//Setting up the selection of the item in the cell.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
delegate?.rightSidebarControlDidSelectRow(indexPath)
}
override func viewDidLoad() {
}
override func didReceiveMemoryWarning() {
}
}
Here is where my problems may start with SideBar:NSObject. This is the left SideBar to be initialized:
import UIKit
#objc protocol SideBarDelegate {
func sideBarDidSelectButtonAtIndex (index: Int)
optional func sideBarWillClose()
optional func sideBarWillOpen()
optional func sideBarWillDeinitialize()
}
//this class sets up the actual sidebar.
class SideBar: NSObject, SidebarTableViewControllerDelegate {
//width of the bar, tableview setup, and views for the sidebar
let barWidth:CGFloat = 175.0
let sideBarTableViewTopInset:CGFloat = 25.0
let sideBarContainerView:UIView = UIView()
let sideBarTableViewController:SidebarTableViewController = SidebarTableViewController()
var originView:UIView!
//var for dynamic effect and controlling the sidebar
var animator:UIDynamicAnimator!
var delegate:SideBarDelegate?
var isSideBarOpen:Bool = false
//initializer for the "SideBar" class.
override init() {
super.init()
}
//initializer for the tableView of menu items.
init(sourceView: UIView, menuItems: Array<String>, menuImages: [UIImage]){
super.init()
//initializing the views and animation for the menu.
originView = sourceView
sideBarTableViewController.tableData = menuItems
sideBarTableViewController.imageData = menuImages
setupSideBar()
animator = UIDynamicAnimator(referenceView: originView)
}
//function for setting up the sidebar.
func setupSideBar () {
//setting up the frame/outline of the side bar.
sideBarContainerView.frame = CGRectMake(-barWidth, originView.frame.origin.y + 45, barWidth, originView.frame.size.height)
//setting up the color of the sidebar.
sideBarContainerView.backgroundColor = UIColor.clearColor()
//disables subviews from being confined to the sidebar.
sideBarContainerView.clipsToBounds = false
//placing the sidebar in the UIView
originView.addSubview(sideBarContainerView)
//adding blur to the menu.
let blurView:UIVisualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.Light))
blurView.frame = sideBarContainerView.bounds
sideBarContainerView.addSubview(blurView)
//setting up controls for the sidebar
sideBarTableViewController.delegate = self
sideBarTableViewController.tableView.frame = sideBarContainerView.bounds
sideBarTableViewController.tableView.clipsToBounds = false
//disabling the scroll feature. Delete to keep the scroll feature.
sideBarTableViewController.tableView.scrollsToTop = false
//This will remove separators in the UITableCell. Delete to keep separators.
sideBarTableViewController.tableView.separatorStyle = UITableViewCellSeparatorStyle.None
//This sets the background color of the sidebar and creates the inset.
sideBarTableViewController.tableView.backgroundColor = UIColor.clearColor()
sideBarTableViewController.tableView.contentInset = UIEdgeInsets(top: sideBarTableViewTopInset, left: 0, bottom: 0, right: 0)
//reloads the sidebar and adds the container view to the sideBarTableViewController.
sideBarTableViewController.tableView.reloadData()
sideBarContainerView.addSubview(sideBarTableViewController.tableView)
}
func showSideBar(shouldOpen: Bool){
animator.removeAllBehaviors()
isSideBarOpen = shouldOpen
//simple if and else statements to define the direction of animation and intensity of animation
let gravityX:CGFloat = (shouldOpen) ? 0.5 : -0.5
let magnitude:CGFloat = (shouldOpen) ? 20 : -20
let boundaryX:CGFloat = (shouldOpen) ? barWidth : -barWidth
//controls the behavior of the animation.
let gravityBehavior: UIGravityBehavior = UIGravityBehavior(items: [sideBarContainerView])
gravityBehavior.gravityDirection = CGVectorMake(gravityX, 0)
animator.addBehavior(gravityBehavior)
let collisionBehavior: UICollisionBehavior = UICollisionBehavior(items: [sideBarContainerView])
collisionBehavior.addBoundaryWithIdentifier("sideBarBoundary", fromPoint: CGPointMake(boundaryX, 20), toPoint: CGPointMake(boundaryX, originView.frame.size.height))
animator.addBehavior(collisionBehavior)
let pushBehavior:UIPushBehavior = UIPushBehavior(items: [sideBarContainerView], mode: UIPushBehaviorMode.Instantaneous)
pushBehavior.magnitude = magnitude
animator.addBehavior(pushBehavior)
let sideBarBehavior:UIDynamicItemBehavior = UIDynamicItemBehavior(items: [sideBarContainerView])
sideBarBehavior.elasticity = 0.3
animator.addBehavior(sideBarBehavior)
}
func sidebarControlDidSelectRow(indexPath: NSIndexPath) {
delegate?.sideBarDidSelectButtonAtIndex(indexPath.row)
}
}
This is the right SideBar:NSObject that will eventually initialize the right menu.
import UIKit
#objc protocol RightSideBarDelegate {
func rightSideBarDidSelectButtonAtIndex (index: Int)
optional func sideBarWillClose()
optional func sideBarWillOpen()
}
class RightSideBar: NSObject, RightSidebarTableViewControllerDelegate {
//width of the bar, tableview setup, and views for the sidebar
let barWidth:CGFloat = 175.0
let rightSideBarTableViewTopInset:CGFloat = 25.0
let rightSideBarContainerView:UIView = UIView()
let rightSideBarTableViewController:RightSidebarTableViewController = RightSidebarTableViewController()
var rightOriginView:UIView!
//var for dynamic effect and controlling the sidebar
var animator:UIDynamicAnimator!
var delegate:RightSideBarDelegate?
var isSideBarOpen:Bool = false
//initializer for the "SideBar" class.
override init() {
super.init()
}
//initializer for the tableView of menu items.
init(rightSourceView: UIView, rightMenuItems: Array<String>, rightMenuImages: [UIImage]){
super.init()
//initializing the views and animation for the menu.
rightOriginView = rightSourceView
rightSideBarTableViewController.rightTableData = rightMenuItems
rightSideBarTableViewController.rightImageData = rightMenuImages
setupSideBar()
animator = UIDynamicAnimator(referenceView: rightOriginView)
}
//function for setting up the sidebar.
func setupSideBar () {
//setting up the frame/outline of the side bar.
rightSideBarContainerView.frame = CGRectMake(rightOriginView.frame.size.width + barWidth , rightOriginView.frame.origin.y + 45, barWidth, rightOriginView.frame.size.height)
//setting up the color of the sidebar.
rightSideBarContainerView.backgroundColor = UIColor.clearColor()
//disables subviews from being confined to the sidebar.
rightSideBarContainerView.clipsToBounds = false
//placing the sidebar in the UIView
rightOriginView.addSubview(rightSideBarContainerView)
//adding blur to the menu.
let blurView:UIVisualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.Light))
blurView.frame = rightSideBarContainerView.bounds
rightSideBarContainerView.addSubview(blurView)
//setting up controls for the sidebar
rightSideBarTableViewController.delegate = self
rightSideBarTableViewController.tableView.frame = rightSideBarContainerView.bounds
rightSideBarTableViewController.tableView.clipsToBounds = false
//disabling the scroll feature. Delete to keep the scroll feature.
rightSideBarTableViewController.tableView.scrollsToTop = false
//This will remove separators in the UITableCell. Delete to keep separators.
rightSideBarTableViewController.tableView.separatorStyle = UITableViewCellSeparatorStyle.None
//This sets the background color of the sidebar and creates the inset.
rightSideBarTableViewController.tableView.backgroundColor = UIColor.clearColor()
rightSideBarTableViewController.tableView.contentInset = UIEdgeInsets(top: rightSideBarTableViewTopInset, left: 0, bottom: 0, right: 0)
//reloads the sidebar and adds the container view to the rightSideBarTableViewController.
rightSideBarTableViewController.tableView.reloadData()
rightSideBarContainerView.addSubview(rightSideBarTableViewController.tableView)
}
func showSideBar(shouldOpen: Bool){
animator.removeAllBehaviors()
isSideBarOpen = shouldOpen
//simple if and else statements to define the direction of animation and intensity of animation
let gravityX:CGFloat = (shouldOpen) ? -0.5 : 0.5
let magnitude:CGFloat = (shouldOpen) ? -20 : 20
let boundaryX:CGFloat = (shouldOpen) ? -barWidth : barWidth
//controls the behavior of the animation.
let gravityBehavior: UIGravityBehavior = UIGravityBehavior(items: [rightSideBarContainerView])
gravityBehavior.gravityDirection = CGVectorMake(gravityX, 0)
animator.addBehavior(gravityBehavior)
let collisionBehavior: UICollisionBehavior = UICollisionBehavior(items: [rightSideBarContainerView])
collisionBehavior.addBoundaryWithIdentifier("sideBarBoundary", fromPoint: CGPointMake(boundaryX, 20), toPoint: CGPointMake(boundaryX, rightOriginView.frame.size.height))
animator.addBehavior(collisionBehavior)
let pushBehavior:UIPushBehavior = UIPushBehavior(items: [rightSideBarContainerView], mode: UIPushBehaviorMode.Instantaneous)
pushBehavior.magnitude = magnitude
animator.addBehavior(pushBehavior)
let sideBarBehavior:UIDynamicItemBehavior = UIDynamicItemBehavior(items: [rightSideBarContainerView])
sideBarBehavior.elasticity = 0.3
animator.addBehavior(sideBarBehavior)
}
func rightSidebarControlDidSelectRow(indexPath: NSIndexPath) {
delegate?.rightSideBarDidSelectButtonAtIndex(indexPath.row)
}
}
Finally this is my current code for the DoubleMenuViewController. Something happens when I segue to the DoubleMenuViewController to break the menus. The menus won't even load. However, if I'm in a SingleMenuViewController that only calls SideBar:NSObject then the code will work so long as I'm only calling one menu. In this DoubleMenuViewController, I have the initialization section commented out for the RightSideBar class because I'm working on a solution. I know this code for this ViewController is garbled. I'm trying everything I can think of. See my remarks after the code to see what I've tried:
import UIKit
class DoubleMenuViewController: UIViewController, SideBarDelegate, RightSideBarDelegate {
var sideBar:SideBar?
var ondemandSideBar:SideBar {
get {
if sideBar == nil {
//setting up the menu items for the sidebar.
sideBar = SideBar(sourceView: self.view, menuItems: ["Home", "Share", "About", "Help"], menuImages: [homeImage!, shareImage!, aboutImage!, helpImage!])
sideBar!.delegate = self
SideBar.new()
}
return sideBar!
}
}
//initializes the "RightSideBar"
var rightSideBar:RightSideBar?
var ondemandRightSideBar:RightSideBar {
get {
if rightSideBar == nil {
rightSideBar = RightSideBar(rightSourceView: self.view, rightMenuItems: [//Other items], rightMenuImages: [//Other Items])
rightSideBar!.delegate = self
RightSideBar.new()
}
return rightSideBar!
}
}
var homeImage = UIImage(named: "Home")
var shareImage = UIImage(named: "Share")
var aboutImage = UIImage(named: "About")
var helpImage = UIImage(named: "Help")
#IBOutlet weak var currentMenuControl: UIBarButtonItem!
#IBAction func currentMenuDisplay(sender: AnyObject) {
if currentMenuControl.tag == 1 {
ondemandSideBar.showSideBar(true)
currentMenuControl.tag = 0
} else {
ondemandSideBar.showSideBar(false)
currentMenuControl.tag = 1
}
}
#IBOutlet weak var progressionMenuControl: UIBarButtonItem!
#IBAction func progressionMenuDisplay(sender: AnyObject) {
if progressionMenuControl.tag == 1 {
ondemandRightSideBar.showSideBar(true)
progressionMenuControl.tag = 0
} else {
ondemandRightSideBar.showSideBar(false)
progressionMenuControl.tag = 1
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func sideBarDidSelectButtonAtIndex(index: Int) {
switch index {
//segues
}
}
func rightSideBarDidSelectButtonAtIndex(index: Int) {
switch index {
//segues
}
}
}
Here's what I've tried:
I've tried altering the positioning of CGFloats since SubViews seem
to come from the left.
I've renamed all the RightSideBar variables and class names
everything to overcome a runtime confusion in instance variables and
class names. This includes renaming the initializers that you saw in
the NSObject SubClass and the target view controller.
I've tried using control flow in the viewDidLoad method with a
button tag. I took away the swipe features to show the menu and
added buttons because I thought system was struggling to deal with
the Swipes.
I've tried deinitializing in the SideBar subclass file of NSObject.
All that got me was an infinite loop that crashed the application
after login.
Then I tried ondemand initialization in the
targetViewController.....DoubleMenuViewController and
SingleMenuViewController. I returned to a working menu with buttons
in the SingleMenuViewController but it still won't show the left and
right menu in the DoubleMenuViewController.
Last I tried deinitializing the SideBar (left SideBar) and the RightSideBar in the DoubleMenuViewController. However, when I add println() functions to all my sections the debugger doesn't run the print function for me to get values of objects or even show typed states like "This". I added the print functions because I wasn't sure if I would know when deinitialization and reinitialization occurred.
It seems that my menu is initialized from the SideBar: NSObject file and the RightSideBar:NSObject file. What I mean is that my menu is being created before I hit the target view controller. This isn't a problem for me so long as I can get the compiler to initialize the SideBar and the RightSideBar in the same View Controller, but it won't do that.
I just need to be able to control both menus with swipes or button taps.
I think I have a problem with my initializers overriding each other.
However, I don't know how to fix that problem. I've read through the Swift manual and read articles on the internet. I've also searched StackOverflow.
You ask:
How do I Initialize two instances of NSObject in the same view controller?
Setting aside why you're dealing with NSObject at all (in Objective-C all classes have to subclass from NSObject ultimately, in Swift that's no longer the case), if you want to instantiate two objects, you simply have to have one property for each.
If these are lazily instantiated, as your code snippet suggests, then you have to identify where that lazily instantiated property is referenced (e.g. you might trigger it from a "swipe from edge" gesture) or what have you. Set a breakpoint in the code that references that lazily instantiated property and make sure you're getting there at all.
--
I had some observations on one of your code snippets. You say that you're instantiating your side bar like so:
var sideBar : SideBar?
var ondemandSideBar : SideBar {
get {
if sideBar == nil {
sideBar = SideBar(sourceView, menuItems, menuImage etc.)
sideBar!.delegate
SideBar.new()
}
}
}
I don't think that's what you're really doing because you're not setting the delegate, you're both instantiating the SideBar as well as calling new (which you shouldn't be doing from Swift), you're not returning a value, etc.
Also, that pattern of having a stored property that is instantiated by some computed property has a certain Objective-C je ne sais quoi. I'm inferring that you want a lazily instantiated property. If that's the case, I'd be inclined to use a single lazy stored property. And I'd then set that property lazily using a closure:
I'd expect something like this pattern
protocol SideBarDelegate : class { // specify class so we can use `weak` reference
func didChooseMenuItem(sender: SideBar, item: Int)
}
class SideBar {
weak var delegate: SideBarDelegate? // make sure this is weak to avoid strong reference cycle
}
class ViewController: UIViewController, SideBarDelegate {
lazy var sideBar: SideBar = {
let _sideBar = SideBar(sourceView, menuItems, menuImage, etc.)
_sideBar.delegate = self
return _sideBar
}()
func didChooseMenuItem(sender: SideBar, item: Int) {
// handle it here
}
// etc.
}
This way, the sideBar won't be instantiated until you reference sideBar somewhere in your code, but when you do, it will be instantiated with the code inside that closure.

IOS Swift: Using delegate methods in custom class

I am trying to create a bar graph class in swift but having difficulty setting up a delegate....heres what I have so far...
BarChart.swift:
import Foundation
import UIKit
#objc protocol BarChartDelegate {
optional func barChart(colorForBarAtIndex index: Int) -> UIColor
}
class BarChart : BarChartDelegate {
let data : [NSDecimalNumber]
let container = UIView()
var barWidth : CGFloat = 100
var barSpacing : CGFloat = 10
var delegate : BarChartDelegate?
init(data: [NSDecimalNumber], frame: CGRect) {
self.data = data
self.container.frame = frame
drawGraph()
}
func drawGraph() {
var i = 0
for item in self.data {
var bar = UIView()
let xPos = CGFloat(i)*self.barWidth
bar.frame = CGRectMake(xPos, 0, self.barWidth, 100)
if let del = delegate? {
bar.frame
println(del.barChart!(colorForBarAtIndex: i))
bar.backgroundColor = del.barChart!(colorForBarAtIndex: i)
}
else {
println("nope!")
}
self.container.addSubview(bar)
i++
}
}
}
ViewController.swift
class ViewController: UIViewController, BarChartDelegate {
var colors = [
UIColor.greenColor(),
UIColor.blueColor(),
UIColor.redColor()
]
override func viewDidLoad() {
super.viewDidLoad()
var barChart = BarChart(data: [NSDecimalNumber(double: 100.00), NSDecimalNumber(double: 200.00), NSDecimalNumber(double: 300.00)], frame: CGRectMake(0, 0, 400.00, 100.00))
self.view.addSubview(barChart.container)
}
func barChart(colorForBarAtIndex index: Int) -> UIColor { // this is not running?
return self.colors[index]
}
}
the delegate method in my ViewController.swift file is not being run and I just get "nope!" printed to the console 3 times... which is a result of the delegate optional finding nil when unwrapping?
Is there something I am missing here?
Any help would be appreciated!
Thanks,
Dave
First off, it rarely makes sense for a BarChart to also be a BarChartDelegate. You don't delegate things to yourself!
Second, as far as I can tell you're not actually setting the ViewController as the delegate of the BarChart. Simply adopting the BarChartDelegate protocol is not enough to do that; you need to set it explicitly.
So, for example, you might want to do this after you create the BarChart:
var barChart = ...
barChart.delegate = self
Alternatively, if the delegate is vital for your chart, you might want to change the constructor to accept a delegate as an argument.

Resources