I'm trying to implement a native-Calendar-app-like timeline view with UICollectionView and custom layout. And I'm new to it.
Here is where my problem come from.
I'm using Decoration View to implement those background gridlines, and trying to use Supplementary View to make the time labels (near the gridlines), and will use Cell to make the events but not that far yet.
But before doing the events, I found when I run it all the supplementary views are not working, no matter if I have cell or not. And I found my collectionView:viewForSupplementaryElementOfKind:atIndexPath: method is not called.
So I'm wondering how and when this method is called? What could be leading to my situation that it's not called?
Actually, is it good to make those time labels with supplementary view? I'm not sure about it since I do need them to be visible even when there's no event (no cell/item in section).
Here is my code:
View Controller
- (void)viewDidLoad
{
[super viewDidLoad];
self.collectionView.backgroundColor = [UIColor whiteColor];
[self.collectionView registerClass:TodayCellKindTask.class forCellWithReuseIdentifier:CellKindTaskIdentifier];
[self.collectionView registerClass:TodayTimelineTimeHeader.class forSupplementaryViewOfKind:TimelineKindTimeHeader withReuseIdentifier:TimelineTimeHeaderIdentifier];
[self.timelineViewLayout registerClass:TodayTimelineTileWhole.class forDecorationViewOfKind:TimelineKindTileWholeHour];
[self.timelineViewLayout registerClass:TodayTimelineTileHalf.class forDecorationViewOfKind:TimelineKindTileHalfHour];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return 5;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
TodayCellKindTask *cellTask = [collectionView dequeueReusableCellWithReuseIdentifier:CellKindTaskIdentifier forIndexPath:indexPath];
return cellTask;
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
NSLog(#"viewForSupplementaryElementOfKind");
TodayTimelineTimeHeader *timeHeader = [self.collectionView dequeueReusableSupplementaryViewOfKind:TimelineKindTimeHeader withReuseIdentifier:TimelineTimeHeaderIdentifier forIndexPath:indexPath];
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDate *today = [NSDate date];
NSDateComponents *comps = [calendar components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:today];
[comps setHour:indexPath.item];
timeHeader.time = [calendar dateFromComponents:comps];
return timeHeader;
}
Custom Layout
// in prepareLayout
NSMutableDictionary *timeHeaderAttributes = [NSMutableDictionary dictionary];
CGSize headerSize = [TodayTimelineTimeHeader defaultSize];
CGFloat headerOffsetY = (tileSize.height - headerSize.height) / 2;
for (NSInteger hour = 24; hour >= 0; hour--) {
NSIndexPath *timeHeaderIndexPath = [NSIndexPath indexPathForItem:hour inSection:0];
UICollectionViewLayoutAttributes *currentTimeHeaderAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:TimelineKindTimeHeader withIndexPath:timeHeaderIndexPath];
CGFloat headerPosY = hour * 2 * tileSize.height + headerOffsetY;
currentTimeHeaderAttributes.frame = CGRectMake(TimeHeaderPosX, headerPosY, headerSize.width, headerSize.height);
timeHeaderAttributes[timeHeaderIndexPath] = currentTimeHeaderAttributes;
}
self.timelineTileAttributes[TimelineKindTimeHeader] = timeHeaderAttributes;
// layoutAttributesForSupplementaryViewOfKind
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
return self.timelineTileAttributes[kind][indexPath];
}
// layoutAttributesForElementsInRect
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:self.timelineTileAttributes.count];
[self.timelineTileAttributes enumerateKeysAndObjectsUsingBlock:^(NSString *elementIdentifier,
NSDictionary *elementsInfo,
BOOL *stop) {
[elementsInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath,
UICollectionViewLayoutAttributes *attributes,
BOOL *stop) {
if (CGRectIntersectsRect(rect, attributes.frame)) {
[allAttributes addObject:attributes];
}
}];
}];
return allAttributes;
}
// collectionViewContentSize
- (CGSize)collectionViewContentSize {
CGSize tileSize = [TodayTimelineTileWhole defaultSize];
CGFloat contentHeight = tileSize.height * self.numberOfTiles;
return CGSizeMake(tileSize.width, contentHeight);
}
I tried not to post all the code here since that'd be a lot, but let me know if you need to know others.
Any tip is appreciated!
Pine
So, this problem is solved, and the reason was my stupid mistake, setting those views' position x out of the screen, and due to that the method was not called. Nothing else.
I found this reason by logging out each view's frame in prepareLayout, and found the x position was wrong. I got this position from the retina design so…
I used supplementary view and it was working without any problem.
My lesson: no worries, calm down. This can save you time from making mistake.
Related
i have no idea why the uiactivityviewcontroller crash when i use the prensentViewController method.
it is weird, anyone has any clue?
the program is running fine not untile when i use the presenviewcontroller.
#import "ActivityViewController.h"
#interface ActivityViewController ()
#end
#implementation ActivityViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
[self createTextField];
[self createButton];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void) createTextField
{
self.myTextField = [[UITextField alloc] initWithFrame:CGRectMake(20.0f, 35.0f, 280.0f, 30.0f)];
self.myTextField.translatesAutoresizingMaskIntoConstraints = NO;
self.myTextField.borderStyle = UITextBorderStyleRoundedRect;
self.myTextField.placeholder =#"Enter text to Share";
self.myTextField.delegate = self;
[self.view addSubview:self.myTextField];
}
- (void) createButton
{
self.myButtonShare = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.myButtonShare.frame = CGRectMake(20.0f, 80.0f, 280.0f, 40.0f);
self.myButtonShare.translatesAutoresizingMaskIntoConstraints = NO;
[self.myButtonShare setTitle:#"Share" forState:UIControlStateNormal];
[self.myButtonShare addTarget:self action:#selector(handleShare:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.myButtonShare];
}
- (void) handleShare:(id)sender
{
NSArray *activities = #[UIActivityTypeMail,UIActivityTypeMessage];
self.myActivityViewController = [[UIActivityViewController alloc] initWithActivityItems:#[self.myTextField.text] applicationActivities:activities];
[self presentViewController:self.myActivityViewController animated:YES completion:nil];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[self.myButtonShare resignFirstResponder];
return YES;
}
What ccjensen said is all correct, except you can actually restrict your share sheet to just Mail & Message by excluding activities, this way for example:
activityController.excludedActivityTypes = #[ UIActivityTypeCopyToPasteboard, UIActivityTypeAddToReadingList ];
This would only show the email sharing by default.
The documentation for UIActivityViewController's initWithActivityItems:applicationActivities: states that the NSArray passed as the second parameter (applicationActivities) should be an "array of UIActivity". You are passing in an array containing the two objects UIActivityTypeMail and UIActivityTypeMessage which are of type NSString *const. It seems that you were hoping that you could restrict the share sheet to only show the mail and messages activity, which is currently not possible.
To stop the crash from happening, change your code to:
<...>
self.myActivityViewController = [[UIActivityViewController alloc] initWithActivityItems:#[self.myTextField.text] applicationActivities:nil];
<...>
I have an app where the user taps a button and the app will instantiate an image layer (UIImageView). I want to make it so that the user can move the selected image layer whichever they tap. After reading some topics here, I've learnt that I can use UIPanGestureRecognizer to move the selected image layer.
- (IBAction)buttonClicked:(id)sender {
imageview = [[UIImageView alloc] initWithFrame:CGRectMake(100, 0, 300, 22)];
UIPanGestureRecognizer *imageviewGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(recognizePan:)];
[imageviewGesture setMinimumNumberOfTouches:1];
[imageviewGesture setMaximumNumberOfTouches:1];
imageview.image = [UIImage imageNamed:#"pinimage.png"];
[imageview addGestureRecognizer:imageviewGesture];
[imageview setUserInteractionEnabled:YES];
[self.view addSubview:imageview];
NSUInteger currentag = [self UniqueTag]; // Assigning a unique integer
imageview.tag = currentag;
}
- (void)recognizePan:(UIPanGestureRecognizer *)sender {
[[[(UITapGestureRecognizer *)sender view] layer] removeAllAnimations];
[self.view bringSubviewToFront:[(UIPanGestureRecognizer *)sender view]];
CGPoint translatedPoint = [(UIPanGestureRecognizer *)sender translationInView:self.view];
if([(UIPanGestureRecognizer *)sender state] == UIGestureRecognizerStateBegan) {
firstX = [[sender view] center].x;
firstY = [[sender view] center].y;
}
translatedPoint = CGPointMake(firstX + translatedPoint.x,firstY + translatedPoint.y);
[[sender view] setCenter:translatedPoint];
}
Now, what I cannot figure out is how to bring the tapped layer to front when there are multiple image layers. It doesn't seem that [self.view bringSubviewToFront:[(UIPanGestureRecognizer *)sender view]] is effective. So how can I revise my code so that the application will bring the tapped layer to the top among others?
Thank you for your help.
A good way to do this is to make a CustomImageView subclass of UIImageView. You attach the UIPanGestureRecognizer to each instance of CustomImageView, and set that instance as it's target. Then the action method triggered by the gesture is implemented in the view itself, so that you can refer to the view with self:
In buttonClicked
MyImageView* imageview = [[MyImageView alloc] initWithFrame:CGRectMake(100, 0, 300, 22)];
UIPanGestureRecognizer *imageviewGesture =
[[UIPanGestureRecognizer alloc] initWithTarget:imageview
action:#selector(recognizePan:)];
In CustomImageView.m
- (void)recognizePan:(UIPanGestureRecognizer *)sender {
[self.layer removeAllAnimations];
[self.superview bringSubviewToFront:self];
CGPoint translatedPoint = [sender translationInView:self];
if([sender state] == UIGestureRecognizerStateBegan) {
self.firstX = [self center].x;
self.firstY = [self center].y;
}
translatedPoint = CGPointMake(self.firstX + translatedPoint.x,
self.firstY + translatedPoint.y);
[self setCenter:translatedPoint];
}
update
Not thinking straight - you can of course do this from the viewController, as you are doing, by accessing the view property of the gestureRecongnizer. Your error is rather here:
- (void)recognizePan:(UIPanGestureRecognizer *)sender {
[[[(UITapGestureRecognizer *)sender view] layer] removeAllAnimations];
You are changing the sender type from UIPanGestureRecognizer to UITapGestureRecognizer. In fact you don't need to do any of that sender typecasting in the body of the method.
I am using TAPKU calendar in my iOS application. I want to add some more marks on date tile after loading complete data in tapku calendar.
I am getting some additional data from async process and I want to mark that data also on calendar.
How can i do this. Thanks in advance.
This is indeed possible, as an example for the day calendar view, modify _refreshDataPageWithAtIndex to be like this:
- (void) _refreshDataWithPageAtIndex:(NSInteger)index{
UIScrollView *sv = self.pages[index];
TKTimelineView *timeline = [self _timelineAtIndex:index];
CGRect r = CGRectInset(self.horizontalScrollView.bounds, HORIZONTAL_PAD, 0);
r.origin.x = self.horizontalScrollView.frame.size.width * index + HORIZONTAL_PAD;
sv.frame = r;
timeline.startY = VERTICAL_INSET;
for (UIView* view in sv.subviews) {
if ([view isKindOfClass:[TKCalendarDayEventView class]]){
[self.eventGraveYard addObject:view];
[view removeFromSuperview];
}
}
if(self.nowLineView.superview == sv) [self.nowLineView removeFromSuperview];
if([timeline.date isTodayWithTimeZone:self.timeZone]){
NSDate *date = [NSDate date];
NSDateComponents *comp = [date dateComponentsWithTimeZone:self.timeZone];
NSInteger hourStart = comp.hour;
CGFloat hourStartPosition = hourStart * VERTICAL_DIFF + VERTICAL_INSET;
NSInteger minuteStart = round(comp.minute / 5.0) * 5;
CGFloat minuteStartPosition = roundf((CGFloat)minuteStart / 60.0f * VERTICAL_DIFF);
CGRect eventFrame = CGRectMake(self.nowLineView.frame.origin.x, hourStartPosition + minuteStartPosition - 5, NOB_SIZE + self.frame.size.width - LEFT_INSET, NOB_SIZE);
self.nowLineView.frame = eventFrame;
[sv addSubview:self.nowLineView];
}
if(!self.dataSource) return;
timeline.events = [NSMutableArray new];
[self.dataSource calendarDayTimelineView:self eventsForDate:timeline.date andEvents:timeline.events success:^{
[timeline.events sortUsingComparator:^NSComparisonResult(TKCalendarDayEventView *obj1, TKCalendarDayEventView *obj2){
return [obj1.startDate compare:obj2.startDate];
}];
[self _realignEventsAtIndex:index];
if(self.nowLineView.superview == sv)
[sv bringSubviewToFront:self.nowLineView];
}];
}
and then change your eventsForDate function to look like this:
- (void) calendarDayTimelineView:(TKCalendarDayView*)calendarDayTimeline eventsForDate:(NSDate *)eventDate andEvents:(NSMutableArray *)events success:(void (^)())success {
[Model doSomethingAsync andSuccess:^(NSArray *classes) {
// .. Add stuff to events..
success();
}];
}
I'm assuming the pattern for the other controls is very similar. The premise is you're waiting to continue the formatting/layout flow til you get your data.
Using iOS6:
I would like to retrieve the text entered by a user into a UITextField associated with the UIAlertView. I am aware that I could achieve the desired result with a delegate however I am curious about solving this issue with a callback function as I believe this may be an interesting pattern. I began by examining a common pattern for category extension of the UIAlertView class. Code below. Thanks in advance for any suggestions.
import <UIKit/UIKit.h>
#interface UIAlertView (Block)
- (id)initWithTitle:(NSString *)title message:(NSString *)message completion:(void (^)(BOOL cancelled, NSInteger buttonIndex, UITextField *textField))completion cancelButtonTitle:(NSString *)cancelButtonTitle otherButtonTitles:(NSString *)otherButtonTitles, ... NS_REQUIRES_NIL_TERMINATION;
#end
The .m for the category follows:
#import "UIAlertView+Block.h"
#import <objc/runtime.h>
static char const * const alertCompletionBlockTag = "alertCompletionBlock";
#implementation UIAlertView (Block)
- (id)initWithTitle:(NSString *)title
message:(NSString *)message
completion:(void (^)(BOOL cancelled, NSInteger buttonIndex))completion
cancelButtonTitle:(NSString *)cancelButtonTitle
otherButtonTitles:(NSString *)otherButtonTitles, ... {
self = [self initWithTitle:title message:message delegate:self cancelButtonTitle:cancelButtonTitle otherButtonTitles:nil ];
if (self) {
objc_setAssociatedObject(self, alertCompletionBlockTag, completion, OBJC_ASSOCIATION_COPY);
va_list _arguments;
va_start(_arguments, otherButtonTitles);
for (NSString *key = otherButtonTitles; key != nil; key = (__bridge NSString *)va_arg(_arguments, void *)) {
[self addButtonWithTitle:key];
}
va_end(_arguments);
}
[self setAlertViewStyle:UIAlertViewStylePlainTextInput];
return self;
}
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
id completion = objc_getAssociatedObject(self, alertCompletionBlockTag);
[self complete:completion index:buttonIndex];
}
- (void) complete:(void (^)(BOOL cancelled, NSInteger buttonIndex))block index:(NSInteger)buttonIndex {
BOOL _cancelled = (buttonIndex == self.cancelButtonIndex);
block(_cancelled, buttonIndex );
objc_setAssociatedObject(self, alertCompletionBlockTag, nil, OBJC_ASSOCIATION_COPY);
//objc_removeAssociatedObjects(block);
}
#end
Usage for the category is set below. The main problem is my inability to reference the UIAlertView textField at Index 0 from within the completion block.
[[[UIAlertView alloc] initWithTitle:#"Add"
message:#"Add New Asset Type"
completion:^(BOOL cancelled, NSInteger buttonIndex){
if (!cancelled) {
//call on completion of UISheetAction ???
NSLog(#"%#",needToAccessUIAlertView._textFields[0]);
}
}
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"OK", nil] show];
So basically you want to access the alert view from the block. You can do something like this:
__block __weak UIAlertView *alertViewWeak;
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Add"
message:#"Add New Asset Type"
completion:^(BOOL cancelled, NSInteger buttonIndex){
if (!cancelled) {
//call on completion of UISheetAction ???
NSLog(#"%#",[alertViewWeak textFieldAtIndex:0]);
}
}
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"OK", nil];
alertViewWeak = alertView;
[alertView show];
If you want to make a category by yourself, above is good enough.
But, there are many classes that uses delegation pattern. Do you want to make categories one by one?
There is REKit. With it, you can use that classes as if they were Block-based:
UIAlertView *alertView;
alertView = [[UIAlertView alloc]
initWithTitle:#"title"
message:#"message"
delegate:nil
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"OK", nil
];
[alertView
respondsToSelector:#selector(alertView:didDismissWithButtonIndex:)
withKey:nil
usingBlock:^(id receiver, UIAlertView *alertView, NSInteger buttonIndex) {
// Do something…
}
];
alertView.delegate = alertView;
Try this library Here is another useful library to do the same. http://ichathan.com/2014/08/19/ichalertview/
I'm trying to get my MKMapView to zoom into to an annotation. I've tried all sorts however I remain zoomed out to my region.
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views
{
NSLog(#"Did add annotations");
MKAnnotationView *annotationView = [views objectAtIndex:0];
id <MKAnnotation> mp = [annotationView annotation];
MKCoordinateSpan span;
span.longitudeDelta = 0.02;
span.latitudeDelta = 0.02;
MKCoordinateRegion region;
region.center = mapView.userLocation.coordinate;
region.span = span;
[mapView selectAnnotation:mp animated:YES];
[self.spotMapView setRegion:region animated:YES];
[self.spotMapView regionThatFits:region];
}
Which does run, however the map stays zoomed out.
Changing didAddAnnotationViews to this:
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views
{
NSLog(#"Did add annotations");
MKAnnotationView *annotationView = [views objectAtIndex:0];
id <MKAnnotation> mp = [annotationView annotation];
[mapView selectAnnotation:mp animated:NO];
}
Seemed to do the trick, the map now zooms in.