How do I generate indentation marker for NSOutlineView?
I am not sure if this is an inbuilt functionality because it appears in other apps like Instruments
Update
I tried solving the problem by iterating all the children of the item that the row represents and show the marker on all children rows based on indentation level, but I faced a few problems
How to handle the case where the item has thousands of children. One simply cannot draw marker to every row as NSOutlineView would draw rows as they are displayed
When I scroll the NSOutlineView, the mouse moves out of the specified row but mouseExited is not being called. Thus the user has to manually move the mouse to reload the highlighting.
I had solved this problem but my solution looks hacky hence wanted to know if there is a better solution. And hence the question
First to receive mouseEntered: and mouseExited: events you need to setup a tracking rect using NSTrackingArea.
I would start with a subclass of NSTableRowView thats overwrites setFrame: making sure the tracking rect gets updated when the view is resize:
#interface TableRowView : NSTableRowView {
NSBox *_box;
NSTrackingArea *_trackingArea;
}
#property (weak) id owner;
#property (copy) NSDictionary<id, id> *userInfo;
#property (nonatomic) CGFloat indentation;
#property (nonatomic) BOOL indentationMarkerHidden;
#end
#implementation TableRowView
- (void)setFrame:(NSRect)frame
{
[super setFrame:frame];
if (_trackingArea) {
[self removeTrackingArea:_trackingArea];
}
_trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options:NSTrackingMouseEnteredAndExited|NSTrackingActiveInKeyWindow owner:[self owner] userInfo:[self userInfo]];
[self addTrackingArea:_trackingArea];
}
#end
To use the NSTableRowView subclass, implement the NSOutlineViewDelegate messages like this:
- (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item
{
TableRowView *view = [[TableRowView alloc] init];
view.owner = self;
view.userInfo = item;
return view;
}
With this in place you're ready to receive mouseEntered: and mouseExited: events. Use NSOutlineView levelForItem: together with indentationPerLevel to calculate the position of the marker NSBox.:
- (void)mouseEntered:(NSEvent *)event
{
id item = [event userData];
CGFloat indentation = [_outlineView levelForItem:item] * [_outlineView indentationPerLevel];
[self setIndentationMarker:indentation hidden:NO item:item];
}
- (void)mouseExited:(NSEvent *)event
{
id item = [event userData];
[self setIndentationMarker:0.0 hidden:YES item:item];
}
Now you get the NSTableRowView subclass by rowViewAtRow:makeIfNecessary: and recursively do the same for all children in your data:
- (void)setIndentationMarker:(CGFloat)indentation hidden:(BOOL)hidden item:(NSDictionary *)item
{
TableRowView *view = [_outlineView rowViewAtRow:[_outlineView rowForItem:item] makeIfNecessary:NO];
view.indentationMarkerHidden = hidden;
view.indentation = indentation;
for (NSMutableDictionary *child in [item objectForKey:#"children"]) {
[self setIndentationMarker:indentation hidden:hidden item:child];
}
}
Now layout the NSBox the NSTableRowView subclass:
#implementation TableRowView
- (instancetype)init
{
self = [super init];
if (self) {
_indentationMarkerHidden = YES;
_box = [[NSBox alloc] init];
_box.boxType = NSBoxCustom;
_box.borderWidth = 0.0;
_box.fillColor = [NSColor tertiaryLabelColor];
_box.hidden = _indentationMarkerHidden;
[self addSubview:_box];
}
return self;
}
- (void)layout
{
[super layout];
NSRect rect = [self bounds];
rect.origin.x = _indentation + 7;
rect.size.width = 10;
_box.frame = rect;
}
- (void)setIndentation:(CGFloat)indentation
{
_indentation = indentation;
[self setNeedsLayout:YES];
}
- (void)setIndentationMarkerHidden:(BOOL)indentationMarkerHidden
{
if (_indentationMarkerHidden != indentationMarkerHidden) {
_indentationMarkerHidden = indentationMarkerHidden;
_box.hidden = indentationMarkerHidden;
}
}
#end
This enough to make a basic version like here:
Related
In building a custom NSButton, I've run into a problem handling highlight behavior. After clicking down on the button, holding, and dragging the cursor outside the button's bounds, mouseExited: and mouseEntered: events are not delivered. I understand the reason why, because in mouseDown: calling [super mouseDown:event]; will block until the click is released.
In researching this I came across this Stack Overflow post which describes the same problem. The solution noted is to add NSTrackingEnabledDuringMouseDrag to the NSTrackingArea options, which I have done, yet I continue to see this problem. I tried the other proposed solution with handling the next events in a loop, but this resulted in odd behavior. The button text color turns black on mouse down instead of highlighting the dimmed color, and it doesn't unhighlight upon releasing the mouse, it remains black.
I am using Xcode 9.3, running on macOS 10.13.4.
Here is my NSButton subclass:
#interface BorderlessButton : NSButton {
NSColor *_tempColor;
}
#property (strong, nonatomic) NSColor *color;
#end
#interface BorderlessButton ()
#property (nonatomic) BOOL pressed;
#end
#implementation BorderlessButton
- (id)init {
if (self = [super init]) {
[self setUp];
}
return self;
}
- (id)initWithFrame:(NSRect)frameRect {
if (self = [super initWithFrame:frameRect]) {
[self setUp];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
[self setUp];
}
return self;
}
- (void)setUp {
_color = [NSColor redColor];
[self setTitle:self.title];
[self setButtonType:NSButtonTypeMomentaryChange];
[self setBordered:NO];
NSTrackingArea *area = [[NSTrackingArea alloc] initWithRect:self.bounds
options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingEnabledDuringMouseDrag
owner:self
userInfo:nil];
[self addTrackingArea:area];
}
- (void)setTitle:(NSString *)title {
[super setTitle:title];
NSMutableAttributedString *colorTitle = [[NSMutableAttributedString alloc] initWithAttributedString:[self attributedTitle]];
[colorTitle addAttributes:#{NSFontAttributeName: self.font, NSForegroundColorAttributeName: self.color} range:NSMakeRange(0, [colorTitle length])];
[self setAttributedTitle:colorTitle];
}
- (void)setColor:(NSColor *)color {
_color = color;
[self setTitle:self.title];
}
- (void)mouseDown:(NSEvent *)event {
self.pressed = YES;
[self highlight:YES];
[super mouseDown:event]; // this blocks until released
[self mouseUp:event];
}
- (void)mouseUp:(NSEvent *)event {
self.pressed = NO;
[self highlight:NO];
[super mouseUp:event];
}
//FIXME: Not called after mouse press down and hold then exit
- (void)mouseExited:(NSEvent *)event {
if (self.pressed) {
[self highlight:NO];
}
[super mouseExited:event];
}
- (void)mouseEntered:(NSEvent *)event {
if (self.pressed) {
[self highlight:YES];
}
[super mouseEntered:event];
}
- (void)highlight:(BOOL)flag {
if (flag) {
if (self.isEnabled) {
NSColor *dimmedColor = [self dimmedColor];
_tempColor = _color;
self.color = dimmedColor;
[self setTitle:self.title];
}
} else {
if (self.isEnabled) {
self.color = _tempColor;
[self setTitle:self.title];
}
}
}
- (NSColor *)dimmedColor {
return [self.color colorWithAlphaComponent:0.5];
}
#end
I am using a Toggle button in XIB,
in AwakeFromNib :I set title and BG image of the button.
Code works very well till OSX 10.10.5 and below but on higher versions when I click on button the text gets removed.
Set attributed Title
[self.deleteButton setWantsLayer:YES];
self.deleteButton.layer.backgroundColor = [self setButtonBGColor];
[self setButtonTitleFor:self.deleteButton
toString:#"Delete"];
[self.deleteButton setLayerContentsRedrawPolicy:NSViewLayerContentsRedrawOnSetNeedsDisplay];
- (void)setButtonTitleFor:(NSButton*)button toString:(NSString*)title
{
NSAttributedString *attrString = [self getAttributedStringForString:title];
[button setAttributedTitle:attrString];
}
Any idea hat should be done.
So finally got it right
Subclassing NSButtonCell helped me out
RPButtonTextTopCell.h
#interface RPButtonTextTopCell : NSButtonCell
#end
RPButtonTextTopCell.m
#implementation RPButtonTextTopCell
-(id) init
{
self = [super init];
if (self) {
}
return self;
}
-(id) initWithCoder:(NSCoder *)decoder
{
return [super initWithCoder:decoder];
}
-(id) initTextCell:(NSString *)string
{
return [super initTextCell:string];
}
-(NSRect)titleRectForBounds:(NSRect)theRect
{
NSRect titleFrame = [super titleRectForBounds:theRect];
NSSize titleSize = [[self attributedStringValue] size];
titleFrame.origin.y = (theRect.origin.y - (theRect.size.height-titleSize.height)*0.5) + 5;
return titleFrame;
}
#end
Utilizing This Custom Class
RPButtonTextTopCell *deleteCell = [[RPButtonTextTopCell alloc] init];
deleteCell.backgroundColor = [self setNSButtonBGColor];
[self.deleteButton setCell:deleteCell];
[self setButtonTitleFor:self.deleteButton
toString:#"Delete"];
[self.deleteButton setAction:#selector(deleteButtonClicked:)];
[self.deleteButton setTarget:self];
and its solved....
So I got a Window:
Window.xib
I got a WindowController too:
WindowController.h reads:
#import <Cocoa/Cocoa.h>
#interface MainWindowController : NSWindowController
{
IBOutlet NSView *firstView;
IBOutlet NSView *secondView;
IBOutlet NSView *thirdView;
int currentViewTag;
}
-(IBAction)switchView:(id)sender;
#end
And the WindowController.m reads:
#import "MainWindowController.h"
#interface MainWindowController ()
#end
#implementation MainWindowController
-(id)init
{
self = [super initWithWindowNibName:#"MainWindow"];
if (self){
// Initialization code here
}
return self;
}
//- (void)windowDidLoad {
// [super windowDidLoad];
//
// // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
//}
#pragma mark - Custom view drawing
-(NSRect)newFrameForNewContentView:(NSView *)view
{
NSWindow *window = [self window];
NSRect newFrameRect = [window frameRectForContentRect:[view frame]];
NSRect oldFrameRect = [window frame];
NSSize newSize = newFrameRect.size;
NSSize oldSize = oldFrameRect.size;
NSRect frame = [window frame];
frame.size = newSize;
frame.origin.y -= (newSize.height - oldSize.height);
return frame;
}
-(NSView *)viewForTag:(int)tag{
NSView *view = nil;
if (tag == 0) {
view = firstView;
} else if (tag == 1) {
view = secondView;
} else {
view = thirdView;
}
return view;
}
-(BOOL) validateToolbarItem:(NSToolbarItem *)item
{
if ([item tag] == currentViewTag) return NO;
else return YES;
}
-(void)awakeFromNib
{
[[self window] setContentSize:[firstView frame].size];
[[[self window] contentView]addSubview:firstView];
[[[self window] contentView]setWantsLayer:YES];
}
-(IBAction)switchView:(id)sender
{
int tag = [sender tag];
NSView *view = [self viewForTag:tag];
NSView *previousView = [self viewForTag:currentViewTag];
currentViewTag = tag;
NSRect newFrame = [self newFrameForNewContentView:view];
[NSAnimationContext beginGrouping];
if ([[NSApp currentEvent] modifierFlags] & NSShiftKeyMask)
[[NSAnimationContext currentContext] setDuration:1.0];
[[[[self window]contentView]animator]replaceSubview:previousView with:view];
[[[self window]animator]setFrame:newFrame display:YES];
[NSAnimationContext endGrouping];
}
#end
The problem I have is that when i switch tabs in my app, the custom view (and there are three of different sizes) draw differently each time. Look at the screenshots, all of the numbers should be centre aligned but they are sometimes and others not. Can anyone see what my error is please?
I will also add that all of the actions have been correctly configured + the code works perfectly if the custom view size is the same all the time.
The view that works
The view that almost works
Again pointing out that in my .xib all of the numbers are aligned to 0x and 0y axis.
Appdelegate.h
#import <Cocoa/Cocoa.h>
#class MainWindowController;
#interface AppDelegate : NSObject <NSApplicationDelegate> {
MainWindowController *mainWindowController;
}
#end
Appdelegate.m
#interface AppDelegate ()
#property (nonatomic) IBOutlet NSWindow *window;
#end
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
-(void)awakeFromNib
{
if(!mainWindowController){
mainWindowController = [[MainWindowController alloc]init];
}
[mainWindowController showWindow:nil];
}
#end
In Interface Builder, make sure to disable the "autoresizes subviews" checkbox of the default view of your window
I'm making a little server app for OS X and I'm using an NSTextView to log some info about connected clients.
Whenever I need to log something I'm appending the new message to the text of the NSTextView this way:
- (void)logMessage:(NSString *)message
{
if (message) {
self.textView.string = [self.textView.string stringByAppendingFormat:#"%#\n",message];
}
}
After this I'd like the NSTextField (or maybe I should say the NSClipView that contains it) to scroll down to show the last line of its text (obviously it should scroll only if the last line is not visible yet, in fact if then new line is the first line I log it is already on the screen so there is no need to scroll down).
How can I do that programmatically?
Found solution:
- (void)logMessage:(NSString *)message
{
if (message) {
[self appendMessage:message];
}
}
- (void)appendMessage:(NSString *)message
{
NSString *messageWithNewLine = [message stringByAppendingString:#"\n"];
// Smart Scrolling
BOOL scroll = (NSMaxY(self.textView.visibleRect) == NSMaxY(self.textView.bounds));
// Append string to textview
[self.textView.textStorage appendAttributedString:[[NSAttributedString alloc]initWithString:messageWithNewLine]];
if (scroll) // Scroll to end of the textview contents
[self.textView scrollRangeToVisible: NSMakeRange(self.textView.string.length, 0)];
}
As of OS 10.6 it's as simple as nsTextView.scrollToEndOfDocument(self).
Swift 4 + 5
let smartScroll = self.textView.visibleRect.maxY == self.textView.bounds.maxY
self.textView.textStorage?.append("new text")
if smartScroll{
self.textView.scrollToEndOfDocument(self)
}
I've been messing with this for a while, because I couldn't get it to work reliably. I've finally gotten my code working, so I'd like to post it as a reply.
My solution allows you to scroll manually, while output is being added to the view. As soon as you scroll to the absolute bottom of the NSTextView, the automatic scrolling will resume (if enabled, that is).
First a category to #import this only when needed...
FSScrollToBottomExtensions.h:
#interface NSView (FSScrollToBottomExtensions)
- (float)distanceToBottom;
- (BOOL)isAtBottom;
- (void)scrollToBottom;
#end
FSScrollToBottomExtensions.m:
#implementation NSView (FSScrollToBottomExtensions)
- (float)distanceToBottom
{
NSRect visRect;
NSRect boundsRect;
visRect = [self visibleRect];
boundsRect = [self bounds];
return(NSMaxY(visRect) - NSMaxY(boundsRect));
}
// Apple's suggestion did not work for me.
- (BOOL)isAtBottom
{
return([self distanceToBottom] == 0.0);
}
// The scrollToBottom method provided by Apple seems unreliable, so I wrote this one
- (void)scrollToBottom
{
NSPoint pt;
id scrollView;
id clipView;
pt.x = 0;
pt.y = 100000000000.0;
scrollView = [self enclosingScrollView];
clipView = [scrollView contentView];
pt = [clipView constrainScrollPoint:pt];
[clipView scrollToPoint:pt];
[scrollView reflectScrolledClipView:clipView];
}
#end
... create yourself an "OutputView", which is a subclass of NSTextView:
FSOutputView.h:
#interface FSOutputView : NSTextView
{
BOOL scrollToBottomPending;
}
FSOutputView.m:
#implementation FSOutputView
- (id)setup
{
...
return(self);
}
- (id)initWithCoder:(NSCoder *)aCoder
{
return([[super initWithCoder:aCoder] setup]);
}
- (id)initWithFrame:(NSRect)aFrame textContainer:(NSTextContainer *)aTextContainer
{
return([[super initWithFrame:aFrame textContainer:aTextContainer] setup]);
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
- (void)awakeFromNib
{
NSNotificationCenter *notificationCenter;
NSView *view;
// viewBoundsDidChange catches scrolling that happens when the caret
// moves, and scrolling caused by pressing the scrollbar arrows.
view = [self superview];
[notificationCenter addObserver:self
selector:#selector(viewBoundsDidChangeNotification:)
name:NSViewBoundsDidChangeNotification object:view];
[view setPostsBoundsChangedNotifications:YES];
// viewFrameDidChange catches scrolling that happens because text
// is inserted or deleted.
// it also catches situations, where window resizing causes changes.
[notificationCenter addObserver:self
selector:#selector(viewFrameDidChangeNotification:)
name:NSViewFrameDidChangeNotification object:self];
[self setPostsFrameChangedNotifications:YES];
}
- (void)handleScrollToBottom
{
if(scrollToBottomPending)
{
scrollToBottomPending = NO;
[self scrollToBottom];
}
}
- (void)viewBoundsDidChangeNotification:(NSNotification *)aNotification
{
[self handleScrollToBottom];
}
- (void)viewFrameDidChangeNotification:(NSNotification *)aNotification
{
[self handleScrollToBottom];
}
- (void)outputAttributedString:(NSAttributedString *)aAttributedString
flags:(int)aFlags
{
NSRange range;
BOOL wasAtBottom;
if(aAttributedString)
{
wasAtBottom = [self isAtBottom];
range = [self selectedRange];
if(aFlags & FSAppendString)
{
range = NSMakeRange([[self textStorage] length], 0);
}
if([self shouldChangeTextInRange:range
replacementString:[aAttributedString string]])
{
[[self textStorage] beginEditing];
[[self textStorage] replaceCharactersInRange:range
withAttributedString:aAttributedString];
[[self textStorage] endEditing];
}
range.location += [aAttributedString length];
range.length = 0;
if(!(aFlags & FSAppendString))
{
[self setSelectedRange:range];
}
if(wasAtBottom || (aFlags & FSForceScroll))
{
scrollToBottomPending = YES;
}
}
}
#end
... You can add a few more convenience methods to this class (I've stripped it down), so that you can output a formatted string.
- (void)outputString:(NSString *)aFormatString arguments:(va_list)aArguments attributeKey:(NSString *)aKey flags:(int)aFlags
{
NSMutableAttributedString *str;
str = [... generate attributed string from parameters ...];
[self outputAttributedString:str flags:aFlags];
}
- (void)outputLineWithFormat:(NSString *)aFormatString, ...
{
va_list args;
va_start(args, aFormatString);
[self outputString:aFormatString arguments:args attributeKey:NULL flags:FSAddNewLine];
va_end(args);
}
I have some customised NSTextView and custom input method so my option was to use:
self.scrollView.contentView.scroll(NSPoint(x: 1, y: self.textView.frame.size.height))
I'm getting an exc_bad_access in my app and I can't figure out how to fix it.
I just upgraded to Xcode 4.5 and I'm targeting IOS 5.0. This is also my first time using UIPageViewController. I prefer to use storyboard as much as possible.
What I'm trying to do is recreate a golf course flip book that contains an image on which you can scroll and zoom. I've basically got a mashup now of several tutorials that is mostly working;
1) I have setup a UIPageviewController, which loads a UIScrollview, which adds a imageView to it.
2) flipping by gesture or tap is working, scrolling is working, I have pinch zoom working and a custom single and two finger tapping working for zoom in / out.
3) The crash appears when you start to flip the page with a sliding gesture, but then release your finger. This basically cancels the flip but then a msg gets send to a zombied object.
Here is my 'GuideViewController.h', it acts as the datasource as well as the root.
#import <UIKit/UIKit.h>
#import "YardageHoleViewController.h"
#interface GuideViewController : UIViewController <UIPageViewControllerDataSource>
#property (strong, nonatomic) UIPageViewController *pageController;
#property (strong, nonatomic) NSArray *pageContent;
- (YardageHoleViewController *)viewControllerAtIndex:(NSUInteger)index storyboard (UIStoryboard *)storyboard;
- (NSUInteger)indexOfViewController:(YardageHoleViewController *)viewController;
#end
And here is the Implementation
#import "GuideViewController.h"
#import "GolfCourseAppDelegate.h"
#import "Hole.h"
#interface GuideViewController ()
#end
#implementation GuideViewController
#synthesize pageContent = _pageContent;
#synthesize pageController = _pageController;
- (void)viewWillDisappear:(BOOL)animated
{
[[[GolfCourseAppDelegate sharedDelegate] locationManager] stopUpdatingLocation];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[[[GolfCourseAppDelegate sharedDelegate] locationManager] startUpdatingLocation];
[self createContentPages];
NSDictionary *options =
[NSDictionary dictionaryWithObject:
[NSNumber numberWithInteger:UIPageViewControllerSpineLocationMin]
forKey: UIPageViewControllerOptionSpineLocationKey];
self.pageController = [[UIPageViewController alloc]
initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl navigationOrientation:UIPageViewControllerNavigationOrientationVertical options: options];
//self.pageController.delegate = self;
self.pageController.dataSource = self;
[[self.pageController view] setFrame:[[self view] bounds]];
YardageHoleViewController *initialViewController = [self viewControllerAtIndex:0 storyboard:self.storyboard];
NSArray *viewControllers = [NSArray arrayWithObject:initialViewController];
[self.pageController setViewControllers:viewControllers
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:NULL];
[self addChildViewController:self.pageController];
[[self view] addSubview:[self.pageController view]];
[self.pageController didMoveToParentViewController:self];
}
- (YardageHoleViewController *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard
{
NSLog(#"getting data view controller at index: %d", index);
// Return the data view controller for the given index.
if (([self.pageContent count] == 0) || (index >= [self.pageContent count])) {
return nil;
}
// Create a new view controller and pass suitable data.
YardageHoleViewController *yardageHoleViewController = [storyboard instantiateViewControllerWithIdentifier:#"YardageHoleViewController"];
yardageHoleViewController.dataObject = [self.pageContent objectAtIndex:index];
return yardageHoleViewController;
}
- (NSUInteger)indexOfViewController:(YardageHoleViewController *)viewController
{
// Return the index of the given data view controller.
// For simplicity, this implementation uses a static array of model objects and the view controller stores the model object; you can therefore use the model object to identify the index.
NSLog(#"returning indexOfViewController : %d", [self.pageContent indexOfObject:viewController.dataObject]);
return [self.pageContent indexOfObject:viewController.dataObject];
}
#pragma mark - Page View Controller Data Source
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
NSLog(#"getting view controller before view controller");
NSUInteger index = [self indexOfViewController:(YardageHoleViewController *)viewController];
if ((index == 0) || (index == NSNotFound)) {
return nil;
}
index--;
return [self viewControllerAtIndex:index storyboard:viewController.storyboard];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
NSLog(#"getting view controller After view controller");
NSUInteger index = [self indexOfViewController:(YardageHoleViewController *)viewController];
if (index == NSNotFound) {
return nil;
}
index++;
if (index == [self.pageContent count]) {
return nil;
}
return [self viewControllerAtIndex:index storyboard:viewController.storyboard];
}
- (void) createContentPages
{
NSLog(#"creating content Pages");
int totalHoles = [[[GolfCourseAppDelegate appData] objectForKey:#"holes"] count];
NSMutableArray *holeData = [[NSMutableArray alloc] init];
for (int i = 1; i < totalHoles+1; i++)
{
Hole *newHole = [[Hole alloc] initWithHoleNumber:i imageUrl:[NSString stringWithFormat:#"hole%#%d.jpg", (i < 10) ? #"0" : #"", i]];
NSLog(#"Hole image url:%#",newHole.imageUrl);
//int holeNumber = i;
//NSString *imageUrl = [NSString stringWithFormat:#"hole%#%d.jpg", (i < 10) ? #"0" : #"", i];
[holeData addObject:newHole];
}
self.pageContent = [[NSArray alloc] initWithArray:holeData];
NSLog(#"count of holeData %d", self.pageContent.count);
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
Now for the view we are flipping through 'YardageHoleViewController.h'
#import <UIKit/UIKit.h>
#import <MediaPlayer/MediaPlayer.h>
#import <CoreLocation/CoreLocation.h>
#interface YardageHoleViewController : UIViewController <UIScrollViewDelegate, CLLocationManagerDelegate>
#property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
#property (assign, nonatomic) int hole;
#property (assign, nonatomic) int totalHoles;
#property (strong, nonatomic) id dataObject;
#property (strong, nonatomic) IBOutlet UILabel *frontLabel;
#property (strong, nonatomic) IBOutlet UILabel *middleLabel;
#property (strong, nonatomic) IBOutlet UILabel *backLabel;
- (IBAction)nextPage:(id)sender;
- (IBAction)previousPage:(id)sender;
- (IBAction)infoPage:(id)sender;
- (IBAction)homePage:(id)sender;
- (void)updateDistanceDisplay;
- (NSString *)formatDistance:(NSNumber *)distance;
#end
Here you can see I've got a few things going on. There is some sub views to show distance to cup based on location etc. You also see some outlets, I wanted to have buttons at the top to navigate in addition to the gestures, right now that's not working because the gestures are overriding the button taps (another question for later).
So here is the meat and potatoes 'YardageHoleViewController.m'
#import "YardageHoleViewController.h"
#import "GolfCourseAppDelegate.h"
#import "Hole.h"
#interface YardageHoleViewController ()
#property (nonatomic, strong) UIImageView *imageView;
- (void)centerScrollViewContents;
- (void)scrollViewDoubleTapped:(UITapGestureRecognizer*)recognizer;
- (void)scrollViewTwoFingerTapped:(UITapGestureRecognizer*)recognizer;
#end
#implementation YardageHoleViewController
#synthesize scrollView = _scrollView;
#synthesize hole = _hole;
#synthesize totalHoles = _totalHoles;
#synthesize imageView = _imageView;
#synthesize frontLabel = _frontLabel;
#synthesize middleLabel = _middleLabel;
#synthesize backLabel = _backLabel;
#synthesize dataObject = _dataObject;
/* The point of this method is to get around a slight annoyance with UIScrollView, which is: if the scroll view content size is smaller than its bounds, then it sits at the top-left rather than in the center. This method positions the image view such that it is always in the center of the scroll view’s bounds.
*/
- (void)centerScrollViewContents {
CGSize boundsSize = self.scrollView.bounds.size;
CGRect contentsFrame = self.imageView.frame;
if (contentsFrame.size.width < boundsSize.width) {
contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0f;
} else {
contentsFrame.origin.x = 0.0f;
}
if (contentsFrame.size.height < boundsSize.height) {
contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0f;
} else {
contentsFrame.origin.y = 0.0f;
}
self.imageView.frame = contentsFrame;
}
- (void)scrollViewDoubleTapped:(UITapGestureRecognizer*)recognizer {
CGPoint pointInView = [recognizer locationInView:self.imageView];
CGFloat newZoomScale = self.scrollView.zoomScale * 1.5f;
newZoomScale = MIN(newZoomScale, self.scrollView.maximumZoomScale);
CGSize scrollViewSize = self.scrollView.bounds.size;
CGFloat w = scrollViewSize.width / newZoomScale;
CGFloat h = scrollViewSize.height / newZoomScale;
CGFloat x = pointInView.x - (w / 2.0f);
CGFloat y = pointInView.y - (h / 2.0f);
CGRect rectToZoomTo = CGRectMake(x, y, w, h);
[self.scrollView zoomToRect:rectToZoomTo animated:YES];
}
- (void)scrollViewTwoFingerTapped:(UITapGestureRecognizer*)recognizer {
// Zoom out slightly, capping at the minimum zoom scale specified by the scroll view
CGFloat newZoomScale = self.scrollView.zoomScale / 1.5f;
newZoomScale = MAX(newZoomScale, self.scrollView.minimumZoomScale);
[self.scrollView setZoomScale:newZoomScale animated:YES];
}
- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView {
// Return the view that you want to zoom
return self.imageView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
// The scroll view has zoomed, so you need to re-center the contents
[self centerScrollViewContents];
}
- (void)viewDidLoad {
[super viewDidLoad];
Hole *hole = (Hole*)self.dataObject;
self.hole = hole.holeNumber;
UIImage *image = [UIImage imageNamed:hole.imageUrl];
self.imageView = [[UIImageView alloc] initWithImage:image];
self.imageView.frame = (CGRect){.origin=CGPointMake(0.0f, 0.0f), .size=image.size};
[self.scrollView addSubview:self.imageView];
self.scrollView.contentSize = image.size;
//Here you’re setting up two gesture recognizers: one for the double-tap to zoom in, and one for the two-finger-tap to zoom out.
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(scrollViewDoubleTapped:)];
doubleTapRecognizer.numberOfTapsRequired = 2;
doubleTapRecognizer.numberOfTouchesRequired = 1;
[self.scrollView addGestureRecognizer:doubleTapRecognizer];
UITapGestureRecognizer *twoFingerTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(scrollViewTwoFingerTapped:)];
twoFingerTapRecognizer.numberOfTapsRequired = 1;
twoFingerTapRecognizer.numberOfTouchesRequired = 2;
[self.scrollView addGestureRecognizer:twoFingerTapRecognizer];
[[[GolfCourseAppDelegate sharedDelegate] locationManager] setDelegate:self];
[self updateDistanceDisplay];
self.totalHoles = [[[GolfCourseAppDelegate appData] objectForKey:#"holes"] count];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(didRotate:) name:#"UIDeviceOrientationDidChangeNotification" object:nil];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
CGRect scrollViewFrame = self.scrollView.frame;
CGFloat scaleWidth = scrollViewFrame.size.width / self.scrollView.contentSize.width;
self.scrollView.minimumZoomScale = scaleWidth;
self.scrollView.maximumZoomScale = 1.5f;
self.scrollView.zoomScale = scaleWidth;
[self centerScrollViewContents];
}
- (void) didRotate:(NSNotification *)notification {
UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
if (orientation == UIDeviceOrientationLandscapeLeft || orientation == UIDeviceOrientationLandscapeRight) {
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleBlackTranslucent];
NSString *moviePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:#"hole%#%d", (self.hole < 10) ? #"0" : #"", self.hole] ofType:#"mp4"];
MPMoviePlayerViewController *viewController = [[MPMoviePlayerViewController alloc] initWithContentURL:[NSURL fileURLWithPath:moviePath]];
viewController.moviePlayer.controlStyle = MPMovieControlStyleNone;
viewController.view.backgroundColor = [UIColor blackColor];
[self presentMoviePlayerViewControllerAnimated:viewController];
} else {
[self dismissMoviePlayerViewControllerAnimated];
}
}
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
[self updateDistanceDisplay];
}
- (void) updateDistanceDisplay {
CLLocation *userLocation = [[GolfCourseAppDelegate sharedDelegate] userLocation];
if (userLocation != nil) {
NSMutableDictionary *holeLocations = [[[GolfCourseAppDelegate appData] objectForKey:#"holes"] objectForKey:[NSString stringWithFormat:#"hole%d", self.hole]];
if (round([[[holeLocations objectForKey:#"front"] objectForKey:#"lat"] floatValue]) == 0) {
self.frontLabel.text = #"---";
} else {
CLLocation *frontLocation = [[CLLocation alloc] initWithLatitude:[[[holeLocations objectForKey:#"front"] objectForKey:#"lat"] floatValue] longitude:[[[holeLocations objectForKey:#"front"] objectForKey:#"lng"] floatValue]];
if (([frontLocation distanceFromLocation:userLocation]/1000)>1000){
self.frontLabel.text = #"Out of Range";
}else{
self.frontLabel.text = [self formatDistance:[NSNumber numberWithFloat:([frontLocation distanceFromLocation:userLocation]/1000)]];
}
}
if (round([[[holeLocations objectForKey:#"middle"] objectForKey:#"lat"] floatValue]) == 0) {
self.middleLabel.text = #"---";
} else {
CLLocation *middleLocation = [[CLLocation alloc] initWithLatitude:[[[holeLocations objectForKey:#"middle"] objectForKey:#"lat"] floatValue] longitude:[[[holeLocations objectForKey:#"middle"] objectForKey:#"lng"] floatValue]];
self.middleLabel.text = [self formatDistance:[NSNumber numberWithFloat:([middleLocation distanceFromLocation:userLocation]/1000)]];
}
if (round([[[holeLocations objectForKey:#"back"] objectForKey:#"lat"] floatValue]) == 0) {
self.backLabel.text = #"---";
} else {
CLLocation *backLocation = [[CLLocation alloc] initWithLatitude:[[[holeLocations objectForKey:#"back"] objectForKey:#"lat"] floatValue] longitude:[[[holeLocations objectForKey:#"back"] objectForKey:#"lng"] floatValue]];
self.backLabel.text = [self formatDistance:[NSNumber numberWithFloat:([backLocation distanceFromLocation:userLocation]/1000)]];
}
}
}
- (NSString *) formatDistance:(NSNumber *)distance {
NSNumber *displayDistance;
NSString *unitSuffix = #"";
// Convert km to yards if prefs say so.
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
if ([[preferences stringForKey:#"measurementUnit"] isEqualToString:#"meters"]) {
distance = [NSNumber numberWithFloat:([distance floatValue]*1000.0)];
if ([distance floatValue] < 1000.0) {
displayDistance = distance;
unitSuffix = #"";
} else {
displayDistance = [NSNumber numberWithFloat:([distance floatValue]/1000.0)];
unitSuffix = #"km";
}
} else {
distance = [NSNumber numberWithFloat:([distance floatValue]*1.0936133*1000.0)];
if ([distance floatValue] < 1760.0) {
displayDistance = distance;
unitSuffix = #"";
} else {
displayDistance = [NSNumber numberWithFloat:([distance floatValue]/1760.0)];
unitSuffix = #"mi";
}
}
NSNumberFormatter *decimalStyle = [[NSNumberFormatter alloc] init];
[decimalStyle setFormatterBehavior:NSNumberFormatterBehavior10_4];
[decimalStyle setNumberStyle:NSNumberFormatterDecimalStyle];
[decimalStyle setRoundingMode:NSNumberFormatterRoundFloor];
[decimalStyle setRoundingIncrement:[NSNumber numberWithFloat:1.0]];
NSString *finalDistance = [decimalStyle stringFromNumber:displayDistance];
return [NSString stringWithFormat:#"%#%#", finalDistance, unitSuffix];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
}
- (void)viewDidUnload {
[self setImageView:nil];
[self setScrollView:nil];
[self setFrontLabel:nil];
[self setBackLabel:nil];
[self setBackLabel:nil];
[self setFrontLabel:nil];
[self setMiddleLabel:nil];
[super viewDidUnload];
}
- (IBAction)nextPage:(id)sender {
//TODO
// [((UIPageViewController*)self.parentViewController) setViewControllers:
// target direction:UIPageViewControllerNavigationForward completion:nil];
}
- (IBAction)previousPage:(id)sender {
//TODO
// [((UIPageViewController*)self.parentViewController) setViewControllers:<#(NSArray *)#> direction:UIPageViewControllerNavigationDirectionReverse animated:true completion:nil];
}
- (IBAction)infoPage:(id)sender {
//TODO
}
- (IBAction)homePage:(id)sender {
[self.navigationController popViewControllerAnimated:YES];
}
#end
Whew! lots of reading. So what I tried first was to set the exception break point. No luck. Then I added lots of NSlog statements to see where we crash, and then finally looked for zombies in instruments. Here we see on the malloc "An Objective-C message was sent to a deallocated object (zombie) at address: 0x1386e0e0" in the YardageHoleViewController.
From my NSLog statements I can see that a successful page turn looks like this;
2012-12-16 13:33:52.280 BAP Template[1365:13a03] getting data view controller at index: 0
//started flip right here >
2012-12-16 13:34:06.289 BAP Template[1365:13a03] getting view controller After view controller
2012-12-16 13:34:06.290 BAP Template[1365:13a03] returning indexOfViewController : 0
2012-12-16 13:34:06.292 BAP Template[1365:13a03] getting data view controller at index: 1
and here is what happens when you start to flip then release
2012-12-16 13:36:18.613 BAP Template[1365:13a03] getting data view controller at index: 0
//started flip then released
2012-12-16 13:36:21.828 BAP Template[1365:13a03] getting view controller After view controller
2012-12-16 13:36:21.829 BAP Template[1365:13a03] returning indexOfViewController : 0
2012-12-16 13:36:21.831 BAP Template[1365:13a03] getting data view controller at index: 1
So in some sense it is trying to act like it completed the flip, but we didn't, and then that's when we have a bad time =(
I've set everything to strong, and I really don't know what to try next?
Any suggestions on my code in general would really be appreciated. Thanks in advance!
UPDATE I looked at the crash log in organizer
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libobjc.A.dylib 0x3737bf78 objc_msgSend + 16
1 CoreLocation 0x3405ddc0 -[CLLocationManager onClientEventLocation:] + 1136
2 CoreLocation 0x3405d77e -[CLLocationManager onClientEvent:supportInfo:] + 194
3 CoreLocation 0x34057e38 __CLClientInvokeCallback_block_invoke_0 + 48
I've since commented out the two lines that start/stop updating location in viewDidLoad / willDisappear in guideViewController.
No more crashing, but why?
Since location updating is started when the view loads, the method
- (void)locationManager:didUpdateToLocation:fromLocation:
gets called continuously in the simulator, like every second, however on the device it only gets called when it detects movement. Inside that method was the call [self updateDistanceDisplay] and that is why it was crashing. The fix I've implemented works well on the device now but it is not bulletproof.
First, in the appDelegate, we want to fix the locationManager.distanceFilter so that not every minor movement triggers the delegate method.
self.locationManager.distanceFilter = 1.0f;
Next, a modification to the didUpdateToLocation method to only update the display if there was a change in the latitude or longitude.
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
if ((newLocation.coordinate.latitude!=oldLocation.coordinate.latitude)||(newLocation.coordinate.longitude!=oldLocation.coordinate.longitude))
[self updateDistanceDisplay];
}
Again, not bulletproof. There is a case where say user is traveling in a golf cart fast enough and trying to flip pages, we could get into the crash state.