iOSHelloMetronome/watchOSMetronome Extension/InterfaceController.m
/* |
Copyright (C) 2017 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
*/ |
#import "InterfaceController.h" |
#import "Metronome.h" |
static const CGFloat kArcWidth = 8.0f; // points |
static const CGFloat kArcGapAngle = (16.0f * M_PI) / 180.0f; // radians |
@interface InterfaceController() <WKCrownDelegate, MetronomeDelegate> { |
NSMutableArray *_foregroundArcArray; |
double _accumulatedRotations; |
BOOL _wasRunning; |
} |
@property (weak, nonatomic) IBOutlet WKInterfaceGroup* backgroundArcsGroup; |
@property (weak, nonatomic) IBOutlet WKInterfaceGroup* foregroundArcsGroup; |
@property (weak, nonatomic) IBOutlet WKInterfaceLabel* tempoLabel; |
@property (weak, nonatomic) IBOutlet WKInterfaceLabel* meterLabel; |
@end |
@implementation InterfaceController |
- (void)awakeWithContext:(id)context { |
[super awakeWithContext:context]; |
// Create and Initialize Metronome Object |
[Metronome sharedInstance]; |
// Draw Background and Foreground Arcs |
[self drawArcs]; |
// Register for Crown Turn Notifications |
[self.crownSequencer setDelegate:self]; |
[self.crownSequencer focus]; |
[[Metronome sharedInstance] setDelegate:self]; |
// if media services are reset, we need to rebuild our audio chain |
[[NSNotificationCenter defaultCenter] addObserver:self |
selector:@selector(handleMediaServicesWereReset:) |
name:AVAudioSessionMediaServicesWereResetNotification |
object:[AVAudioSession sharedInstance]]; |
} |
- (void)willActivate { |
// This method is called when watch view controller is about to be visible to user |
[super willActivate]; |
[self updateMeterLabel]; |
[self updateTempoLabel]; |
} |
- (void)didDeactivate { |
// This method is called when watch view controller is no longer visible |
[[Metronome sharedInstance] stopClock]; |
[super didDeactivate]; |
} |
#pragma mark - Metronome Delegate |
- (void)metronomeTick:(NSInteger)currentTick { |
[self updateArcWithTick:currentTick]; |
} |
#pragma mark - WKCrownSequencer Delegate |
- (void)crownDidRotate:(WKCrownSequencer *)crownSequencer rotationalDelta:(double)rotationalDelta { |
if ([[Metronome sharedInstance] isRunning]) { |
[[Metronome sharedInstance] stopClock]; |
[self updateArcWithTick:0]; |
_wasRunning = YES; |
} |
NSInteger value = 0; |
_accumulatedRotations += rotationalDelta; |
if (_accumulatedRotations >= 0.15) { |
value = 1; |
_accumulatedRotations = 0; |
} else if (_accumulatedRotations <= -0.15) { |
value = -1; |
_accumulatedRotations = 0; |
} |
if (value) { |
[[Metronome sharedInstance] incrementTempo:value]; |
[self updateTempoLabel]; |
} |
} |
- (void)crownDidBecomeIdle:(nullable WKCrownSequencer *)crownSequencer { |
if (_wasRunning) { |
[[Metronome sharedInstance] startClock]; |
_wasRunning = NO; |
} |
} |
#pragma mark - User Interface Methods |
- (IBAction)tapGestureRecognized:(id)sender { |
if ([[Metronome sharedInstance] isRunning]) { |
[[Metronome sharedInstance] stopClock]; |
[self updateArcWithTick:0]; |
} else { |
[[Metronome sharedInstance] startClock]; |
} |
} |
- (IBAction)swipeRightGestureRecognized:(id)sender { |
BOOL wasRunning = [[Metronome sharedInstance] isRunning]; |
if (wasRunning) { |
[[Metronome sharedInstance] stopClock]; |
} |
[[Metronome sharedInstance] incrementMeter:-1]; |
[self drawArcs]; |
[self updateMeterLabel]; |
if (wasRunning) { |
[[Metronome sharedInstance] startClock]; |
} |
} |
- (IBAction)swipeLeftGestureRecognized:(id)sender { |
BOOL wasRunning = [[Metronome sharedInstance] isRunning]; |
if (wasRunning) { |
[[Metronome sharedInstance] stopClock]; |
} |
[[Metronome sharedInstance] incrementMeter:+1]; |
[self drawArcs]; |
[self updateMeterLabel]; |
if (wasRunning) { |
[[Metronome sharedInstance] startClock]; |
} |
} |
- (IBAction)swipeUpGestureRecognized:(id)sender { |
[[Metronome sharedInstance] incrementDivisionIndex:+1]; |
[self updateMeterLabel]; |
} |
- (IBAction)swipeDownGestureRecognized:(id)sender { |
[[Metronome sharedInstance] incrementDivisionIndex:-1]; |
[self updateMeterLabel]; |
} |
#pragma mark - Private Methods |
- (void)updateArcWithTick:(NSInteger)currentTick { |
if ([[Metronome sharedInstance] isRunning]) { |
[self.foregroundArcsGroup setBackgroundImage:(UIImage*)[_foregroundArcArray objectAtIndex:currentTick]]; |
} else { |
[self.foregroundArcsGroup setBackgroundImage:NULL]; |
} |
} |
- (void)updateMeterLabel { |
[self.meterLabel setText:[NSString stringWithFormat:@"%d / %d", (int)[[Metronome sharedInstance] meter], (int)[[Metronome sharedInstance] division]]]; |
} |
- (void)updateTempoLabel { |
[self.tempoLabel setText:[NSString stringWithFormat:@"%d BPM", (int)[[Metronome sharedInstance] tempo]]]; |
} |
#pragma mark - Drawing Methods |
- (void)drawArcs { |
NSUInteger meter = [[Metronome sharedInstance] meter]; |
CGFloat scale = [WKInterfaceDevice currentDevice].screenScale; |
CGColorRef foregroundFillColor = [UIColor colorWithRed:0.301f green:0.556f blue:0.827f alpha:1.0f].CGColor; |
CGColorRef firstElementFillColor = [UIColor colorWithRed:0.301f green:0.729f blue:0.478f alpha:1.0f].CGColor; |
CGColorRef backgroundFillColor = [UIColor colorWithRed:0.5f green:0.5f blue:0.5f alpha:1.0f].CGColor; |
CGFloat contentFrameWidth = self.contentFrame.size.width; |
CGFloat contentFrameHeight = self.contentFrame.size.height; |
CGPoint center = CGPointMake(contentFrameWidth / 2.0, contentFrameHeight / 2.0); |
CGFloat radius = MIN(contentFrameWidth / 2.0, contentFrameHeight/ 2.0) - (kArcWidth/2.0f); |
CGFloat stepAngle = ((2.0f * M_PI) / meter) - kArcGapAngle; |
// Draw Background Rings |
CGFloat startAngle = (kArcGapAngle / 2.0f) - (1.5f * M_PI_2); |
UIGraphicsBeginImageContextWithOptions(self.contentFrame.size, false, scale); |
CGContextRef context = UIGraphicsGetCurrentContext(); |
CGContextBeginPath(context); |
for (NSUInteger i = 0; i < meter; i++) { |
CGPathRef strokedArc = [self newDonutArcWithCenter:center withRadius:radius fromStartAngle:startAngle toEndAngle:startAngle + stepAngle]; |
CGContextAddPath(context, strokedArc); |
CGPathRelease(strokedArc); |
startAngle += stepAngle + kArcGapAngle; |
} |
CGContextClosePath(context); |
CGContextSetFillColorWithColor(context, backgroundFillColor); |
CGContextFillPath(context); |
CGImageRef cgBackgroundImage = CGBitmapContextCreateImage(context); |
UIImage* backgroundImage = [UIImage imageWithCGImage:cgBackgroundImage]; |
[self.backgroundArcsGroup setBackgroundImage:backgroundImage]; |
CGImageRelease(cgBackgroundImage); |
UIGraphicsEndImageContext(); |
// Draw and Store Foreground Rings |
[_foregroundArcArray removeAllObjects]; |
_foregroundArcArray = nil; |
_foregroundArcArray = [[NSMutableArray alloc] init]; |
startAngle = (kArcGapAngle / 2.0f) - (1.5f * M_PI_2); |
for (NSUInteger i = 0; i < meter; i++) { |
UIGraphicsBeginImageContextWithOptions(self.contentFrame.size, false, scale); |
context = UIGraphicsGetCurrentContext(); |
CGContextBeginPath(context); |
CGPathRef strokedArc = [self newDonutArcWithCenter:center withRadius:radius fromStartAngle:startAngle toEndAngle:startAngle+stepAngle]; |
CGContextAddPath(context, strokedArc); |
CGPathRelease(strokedArc); |
CGContextClosePath(context); |
if (i==0) { |
CGContextSetFillColorWithColor(context, firstElementFillColor); |
} else { |
CGContextSetFillColorWithColor(context, foregroundFillColor); |
} |
CGContextFillPath(context); |
CGImageRef cgImage = CGBitmapContextCreateImage(context); |
UIImage* foregroundImage = [UIImage imageWithCGImage:cgImage]; |
[_foregroundArcArray addObject:foregroundImage]; |
CGImageRelease(cgImage); |
UIGraphicsEndImageContext(); |
startAngle += stepAngle + kArcGapAngle; |
} |
} |
- (CGPathRef)newDonutArcWithCenter:(CGPoint)centerPoint withRadius:(CGFloat)radius fromStartAngle:(CGFloat)startAngle toEndAngle:(CGFloat)endAngle { |
CGMutablePathRef arc = CGPathCreateMutable(); |
CGPathAddArc(arc, NULL, |
centerPoint.x, centerPoint.y, |
radius, |
startAngle, |
endAngle, |
NO); |
CGPathRef strokedArc = CGPathCreateCopyByStrokingPath(arc, NULL, kArcWidth, kCGLineCapSquare, |
kCGLineJoinMiter, |
10); // 10 is default miter limit |
CGPathRelease(arc); |
return strokedArc; |
} |
#pragma mark- AVAudioSession Notifications |
// see https://developer.apple.com/library/content/qa/qa1749/_index.html |
- (void)handleMediaServicesWereReset:(NSNotification *)notification |
{ |
NSLog(@"Media services have reset..."); |
// reset |
[[Metronome sharedInstance] setDelegate:nil]; |
[[Metronome sharedInstance] reset]; |
// reset label and draw background and foreground arcs |
[self updateMeterLabel]; |
[self drawArcs]; |
[[Metronome sharedInstance] setDelegate:self]; |
NSError *error = nil; |
[[AVAudioSession sharedInstance] setActive:YES error:&error]; |
if (error) { |
NSLog(@"AVAudioSession error %d, %@", error.code, error.localizedDescription); |
} |
} |
@end |
Copyright © 2017 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2017-02-24