iOS adding subviews to a scroll view - ios

I'm using a double for loop to add UIButtons to a UIScrollView in a grid format. These UIButtons take time to load as they have subviews that are UIImageViews which get their UIImages by downloading data off the internet.
Right now, the subviews don't show until AFTER the method completely finishes executing. Correct me if I'm wrong, but I'm guessing xcode doesn't show added subviews until a method is done executing.
However, I do want to show each subview getting added one at a time, as a cool loading effect. How would I implement this?
Thanks!

You should use multiple threads to load your pictures so that your main thread does not become sluggish. I recently wrote something similar...Take a look at my code from my viewWillAppear method:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.myImages = [self.myModel retrieveAttachments]; //Suppose this takes a long time
for (UIImage *image in self.myImages)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self addImageToScrollView:image animated:YES]; });
}
}
});
The addImageToScrollView method would be like so:
-(void) addImageToScrollView: (UIImage *) image animated: (BOOL) animated
{
//Create image view
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
imageView.image = image;
if(animated)
{
imageView.alpha = 0;
[self.myScrollView addSubview:imageView];
[UIView animateWithDuration:ADD_IMAGE_APPEARING_ANIMATION_SPEED animations:^{
imageView.alpha = 1;
}];
}
else
{
[self.myScrollView addSubview:imageView];
}
}

Related

Is loading UI in background thread okay?

My app is presently made of a TableViewController, and a ViewController. When a cell in the TableView is selected, the ViewController is pushed, which is the main app. This View Controller previously loads all its UIViews in the main thread, which caused the screen to freeze as the code ran, often leading users to believe it has crashed. To prevent this issue, and improve the user experience, I have changed my code to the following overall format:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self initialiseApp];
}
- (void) initialiseApp {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Initialising views
imageView = [[UIImageView alloc] initWithImage:[self getImageFromUrl:currentWallpaperURL]];
[imageView setFrame:CGRectMake(0.0, 100, screenWidth, (screenWidth/7)*4)];
[imageView setContentMode:UIViewContentModeScaleAspectFit];
// etc etc for other views
dispatch_async(dispatch_get_main_queue(), ^{
//Add subviews to UI
[[self view] addSubview:imageView];
});
});
}
When app running in the simulator, this causes the ViewController to load as a blank screen, and later load the UI after some time. During loading, I would have some form of spinner, or text on the screen. I would therefore like clarification on this topic:
Is loading an app's UI when the ViewController is opened (or alternatively, when the app is launched) conventional? And if not, what is a better alternative to prevent having the app freeze for 10 seconds on launch?
Thanks.
I tend to experience issues when creating UI elements on background threads, so I would avoid this if possible (Apple says the same). In your situation, rather than loading the UI elements in the background, load the image in the background and then create the UI elements when the image has been loaded. As an example,
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *image = [self getImageFromUrl:currentWallpaperURL];
dispatch_async(dispatch_get_main_queue(), ^{
//Add subviews to UI
imageView = [[UIImageView alloc] initWithImage:image];
[imageView setFrame:CGRectMake(0.0, 100, screenWidth, (screenWidth/7)*4)];
[imageView setContentMode:UIViewContentModeScaleAspectFit];
[[self view] addSubview:imageView];
});
});

Adding larg number of Buttons to UIScrollView in background thread - Buttons not visible

the user should be able to select one icon from a large number of different icons. I have created a picker dialog that allows the user to make his selection. The ViewController that is used for this picker only holds one UIScrollView. In viewDidLoad for each icon a button is added to the ScrollView. To select an icon the user just has to click the corresponding button...
This works fine, but the ViewController/picker needs several seconds to be displayed. This is because of the many alloc / add operations within viewDidLoad. Because of this I tried to move these options into a background thread. This workes fine, but the created buttons are not visible any more:
- (void)viewDidLoad {
[super viewDidLoad];
self.iconsScrollView.hidden = true;
[self.activityIndicator startAnimating];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
iconContainer = [[UIView alloc] init];
iconContainer.backgroundColor = [UIColor clearColor];
iconButtons = [[NSMutableDictionary alloc] init];
CGRect buttonRect = CGRectMake(5, 5, 40, 40);
selectedButton = nil;
NSArray *iconInfos = [[StoreController sharedController] allIcons];
for (IconInfo* iconInfo in iconInfos) {
NSString *iconName = iconInfo.name;
UIButton *iconButton = [UIButton buttonWithType:UIButtonTypeCustom];
iconButton.frame = buttonRect;
[iconButton addTarget:self action:#selector(iconSelectionClick:) forControlEvents: UIControlEventTouchUpInside];
[iconButton setImage:[UIImage imageNamed:iconName] forState:UIControlStateNormal];
[iconContainer addSubview:iconButton];
[iconButtons setObject:iconButton forKey:iconInfo.guid];
buttonRect.origin.x += 50;
if (buttonRect.origin.x > 205) {
buttonRect.origin.x = 5;
buttonRect.origin.y += 50;
}
}
iconContainer.frame = CGRectMake(0, 0, self.iconsScrollView.frame.size.width, ceil([iconButtons count] / 5.0) * 50);
dispatch_async(dispatch_get_main_queue(), ^{
[self.iconsScrollView addSubview:iconContainer];
self.iconsScrollView.contentSize = iconContainer.frame.size;
[self.activityIndicator stopAnimating];
self.iconsScrollView.hidden = false;
[self.view setNeedsDisplay];
});
});
}
This works (almost) without any problem:
Picker ViewController is presented
ActivityIndicator is visible while buttons are created
Once all buttons are ready ActivityIndicator stops and the ScrollView becomes visible.
Only Problem: The Buttons are not visible. The ScrollView can be used normally (content size correct) and when I touch inside the ScrollView and hit an invisible button the click selector is called. Thus all buttons are there but not visible. Eventually after 10-15 seconds all Buttons become visible at once.
Using setNeedsDisplay or setNeedsLayout for the View, the ScrollView, or the buttons does not change anything.
Any idea what I can do?
You are adding buttons to a subview while you aren't on the main thread.
Generally, UIKit code should only be run on the main queue.
UIKit can only be updated from the main thread
dispatch_async(dispatch_get_main_queue(), ^(void){
//Run UI Updates
});

iOS dynamic object creation and storage

What I have is a UIButton. What I want to do is, every time the button is clicked, a new UIImageView is created (I will set the size/image etc... in a method) and the UIImageView falls to the bottom of the screen. How do I do this? I've tried creating the object and storing it in an NSMutableArray and doing a for loop on the array but that doesn't seem to work. (example of what I'm doing)
-(IBAction) button {
[self createUiImage];
}
-(void) createUiImage {
UIIMageView *iv = [[UIImageView alloc] initWithFrame:CGRectMake(50, 50, 15, 3)]
iv.image = [UIImage imageNamed:#"image.png"];
iv.hidden = NO;
[self.view addSubview:iv];
[array1 addObject:iv];
}
-(void) dropImageDown {
for (UIImageView *a in array1) {
a.center = CGPointMake(a.center.x, a.center.y + 10);
if (a.center.y > 500) {
[array1 removeAllObjects];
[a removeFromSuperview];
}
}
}
and this dropImageDown method is being controlled by an NSTimer.
forgot to add this: The problem is that the shape isn't falling to the bottom, it appears and doesn't move!
I've also tried a for (int i = 0; i < array size; i ++) but thats not working either
Appreciate the help, thanks
Don't try to use a timer and move the view in steps yourself. It will give jerky animation that puts a large burden on the CPU.
Use UIView animation instead.
Something like this:
-(void) animateImagesDown {
for (UIImageView *a in array1)
{
[UIView animateWithDuration: 1.0
animations: ^
{
a.center = CGPointMake(a.center.x, 500);
}
completion: ^(BOOL finished)
{
[a removeFromSuperview];
}
];
}
}
That code would animate all the view to a center position of 500, then remove them. (They would all be animated at the same time.)
UIView animations use ease-in, ease-out animation by default. There are other variations on the animateWithDuration UIView animation methods that let you control the animation timing. (look at the docs on the methodanimateWithDuration:delay:options:animations:completion:(Specifically the options parameter.)
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithObjects/WorkingwithObjects.html use this link which can be help to understand the concept
Found the reason I was having issues... I was not initializing the NSMutableArray therefore it wasn't being recognized.

Destroy UIImageView objects after they move out of bounds

In my application I have some images falling from above inside a UIView. The UIImageView objects are created dynamically and made to move downwards. My question is, do the objects destroy themselves after they move off below the screen area? Or should I do it manually to improve performance?
Once there are no more strong references to an object, it will be deallocated. The two references you probably need clear will be the variable pointer, and the superview's reference to it. So you'd need to do something like:
[imageView removeFromSuperView];
imageView = nil;
It's possible there are more as you did not provide any code (if for instance you have a pointer to the object in an array, you'd need to remove that too).
Removing from superview is all that's needed if you don't have any other pointers to the image view (in ARC). The most performant pattern is to keep a pool of pointers to offscreen image views. In pseudo code:
// this assumes the number onscreen is relatively small, in the tens or low hundreds
// this works better when the number on screen is relatively constant (low variance)
// say the animation looks something like this:
- (void)makeAnImageFall:(UIImage *)image {
CGRect startFrame = // probably some rect above the superview's bounds
UIImageView *imageView = [self addImageViewWithImage:image frame:frame]; // see below
[UIView animateWithDuration:1.0 animations:^{
imageView.frame = // probably some rect below the superview's bounds
} completion:^(BOOL finished) {
[self removeImageView:imageView]; // see below
}];
}
Then these methods do everything to handle the pool:
- (UIImageView *)addImageViewWithImage:(UIImage *)image frame:(CGRect)frame {
UIImageView *imageView;
// assume you've declared and initialized imageViewPool as an NSMutableArray
// or do that here:
if (!self.imageViewPool) self.imageViewPool = [NSMutableArray array];
if (self.imageViewPool.count) {
imageView = [self.imageViewPool lastObject];
[self.imageViewPool removeLastObject];
} else {
imageView = [[UIImageView alloc] init];
}
imageView.image = image;
imageView.frame = frame;
[self.view addSubview:imageView];
return imageView;
}
- (void)removeImageView:(UIImageView *)imageView {
imageView.image = nil;
[self.imageViewPool addObject:imageView];
[imageView removeFromSuperview];
}
The nice idea is that we avoid the relatively expensive churn of create-destroy of many image views. Instead, we create them (lazily) when they're first needed. Once we hit the high water mark of image views, we don't ever allocate another.

Scroll view is not smooth

I'm working in a project (iOS7 & ARC) in which, I want to display N number of images in the scroll view.These Images already stored into sandbox directory. My App has only landscape orientation I'm facing a problem that ScrollView is not smooth, it stuck after 2-3 times scroll
This is how I configure ScrollView
[self.containerScroll setAutoresizesSubviews:NO];
self.containerScroll.pagingEnabled = YES;
self.containerScroll.showsHorizontalScrollIndicator = NO;
self.containerScroll.showsVerticalScrollIndicator = NO;
self.containerScroll.scrollsToTop = NO;
self.containerScroll.maximumZoomScale = 5.0;
self.containerScroll.minimumZoomScale = 1.0;
self.containerScroll.delegate = self;
I'm maintaining only three Images in the scrollView at a time.
I'm loading Images in ScrollView in below method
-(void) loadScrollViewWithPage:(int) page{
if (page >= self.numberOfSlides)
return;
float image_width;
float image_height;
if(self.isFromListView){
if(IS_IPHONE5){
image_width = 568.0f;
image_height = 320.0f;
} else{
// iPhone retina-3.5 inch
image_width = 480.0f;
image_height = 320.0f;
}
}
else{
image_width = IMAGE_WIDTH;
image_height = IMAGE_HEIGHT;
}
CGFloat xPos = page * image_width;
UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(xPos, 0.0f, image_width, image_height)];
imgView.tag = page;
NSString *imgPath = [self.storageDirPath stringByAppendingPathComponent:[NSString stringWithFormat:#"%d%#", page, Image_Extension_JPG]];
NSFileManager *fileManager = [NSFileManager defaultManager];
__block UIImage *img = nil;
if(![fileManager fileExistsAtPath:imgPath]){
[imgView setContentMode:UIViewContentModeCenter];
img = [UIImage imageNamed:#"icon-loader.png"];
[imgView setImage:img];
}
else{
[imgView setContentMode:UIViewContentModeScaleAspectFit];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
img = [[UIImage alloc] initWithCGImage:[[UIImage imageWithData:[NSData dataWithContentsOfFile:imgPath]] CGImage] scale:1.0 orientation:UIImageOrientationUp];
dispatch_async(dispatch_get_main_queue(), ^{
[imgView setImage:img];
});
});
}
[self.containerScroll addSubview:imgView];
img = nil;
fileManager = nil;
imgView = nil;
}
and this how my ScrollView Delegate methods goes...
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
self.containerScroll.scrollEnabled = YES;
float page = self.containerScroll.contentOffset.x/self.view.frame.size.width;
showingSlide = (UInt16) roundf(page);
if(scrollView == self.containerScroll){
// switch the indicator when more than 50% of the previous/next page is visible
CGFloat pageWidth = CGRectGetWidth(self.containerScroll.frame);
NSUInteger pageNo = floor((self.containerScroll.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
// load the visible page and the page on either side of it (to avoid flashes when the user starts scrolling)
[self loadScrollViewWithPage:pageNo - 1];
[self loadScrollViewWithPage:pageNo];
[self loadScrollViewWithPage:pageNo + 1];
// a possible optimization would be to unload the views+controllers which are no longer visible
if(scrollView == self.containerScroll)
{
[self.previewTableView reloadData];
[self.previewTableView setContentOffset:CGPointMake(0, (page*220)+64) animated:NO];
[self.previewTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:page inSection:0] atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
[self updateSlideNumber];
[self flashSlideNumber];
}
//unload unnecessary imageviews from scroll view
for (UIView* view in self.containerScroll.subviews) {
if ([view isKindOfClass:[UIImageView class]] && view.tag != page && view.tag != page-1 && view.tag != page+1) {
[view removeFromSuperview];
}
}
}
}
Now the problem is smoothness of scrollView. When I start scrolling it scrolls fine but after 2 or 3 (or after any random number) pages scroll, it stuck and after trying 2-3 times only it moves again and I have to swipe hard to scroll. Thanks in advance.
I think it's a problem of memory somewhere Try #autorelease pool in your code.
using scrollview is not a good approach for showing images, I will recommend you to either use tableview or collevtionview for the same.
Your app's memory will keep on increasing with every scroll because scollview doesn't reuse the memory, on the other hand tableview and collectionview reuses the memory.
As the most effective way to become better, scroll really slowly (one at a time) while you monitor the memory usage in your App. You'll be able to watch it go up as each new image is added to the view, especially if you haven't done any optimisation on the images.
The other thing is that while your code does look like is deallocc-ing the images, you still need to remember that it still has to try to reload the images as you scroll. You're creating your images on the main thread so you're never going to get the smoothness of a UITableView. While I realise that you're creating your image views on async threads, the act of adding and scrolling them is still being taken care of by the mainthread.
I would suggest a UITableView to solve your problem, or a UICollectionView. If you're set on using the scrollview, I would suggest using a crusher of some type to get the image size to as small as possible, while still keeping quality decent.
If you need help on the TableView implementation you should find plenty of information around SO. Probably a good option if you still want it to look like a scroll view is just to make all seperators, headers etc to nil, and then just use lazy loading for the images.
You make two the mistake. At first: never use imageNamed for non graphics content (example, use imageNamed for button background). And second: you try load a big images in real time. So you scroll view have lags therefore. If you load all images before you show the scroll view the amination end lagging. But you can get memory warnings. So, you need optimise it. P.S. Sorry for my english

Resources