AVCustomEdit/APLViewController.m
/* |
Copyright (C) 2017 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
UIViewController subclasses which handles setup, playback and export of AVMutableComposition along with other user interactions like scrubbing, toggling play/pause, selecting transition type. |
*/ |
#import <AVFoundation/AVFoundation.h> |
#import <CoreMedia/CoreMedia.h> |
#import <MobileCoreServices/MobileCoreServices.h> |
#import <Photos/Photos.h> |
#import "APLViewController.h" |
#import "APLSimpleEditor.h" |
/* |
Player view backed by an AVPlayerLayer |
*/ |
@interface APLPlayerView : UIView |
@property (nonatomic, strong) AVPlayer *player; |
@end |
@implementation APLPlayerView |
+ (Class)layerClass |
{ |
return [AVPlayerLayer class]; |
} |
- (AVPlayer *)player |
{ |
return [(AVPlayerLayer *)[self layer] player]; |
} |
- (void)setPlayer:(AVPlayer *)player |
{ |
[(AVPlayerLayer *)[self layer] setPlayer:player]; |
} |
@end |
static NSString* const AVCustomEditPlayerViewControllerStatusObservationContext = @"AVCustomEditPlayerViewControllerStatusObservationContext"; |
static NSString* const AVCustomEditPlayerViewControllerRateObservationContext = @"AVCustomEditPlayerViewControllerRateObservationContext"; |
@interface APLViewController () |
@property APLSimpleEditor *editor; |
@property NSMutableArray *clips; |
@property NSMutableArray *clipTimeRanges; |
@property AVPlayer *player; |
@property AVPlayerItem *playerItem; |
- (void)updatePlayPauseButton; |
- (void)updateScrubber; |
- (void)updateTimeLabel; |
- (CMTime)playerItemDuration; |
- (void)synchronizePlayerWithEditor; |
@property IBOutlet APLPlayerView *playerView; |
@property IBOutlet UIToolbar *toolbar; |
@property IBOutlet UISlider *scrubber; |
@property IBOutlet UIBarButtonItem *playPauseButton; |
@property IBOutlet UIBarButtonItem *transitionButton; |
@property IBOutlet UIBarButtonItem *exportButton; |
@property IBOutlet UILabel *currentTimeLabel; |
@property IBOutlet UIProgressView *exportProgressView; |
- (IBAction)handleTapGesture:(UITapGestureRecognizer *)tapGestureRecognizer; |
- (IBAction)togglePlayPause:(id)sender; |
- (IBAction)exportToMovie:(id)sender; |
- (IBAction)beginScrubbing:(id)sender; |
- (IBAction)scrub:(id)sender; |
- (IBAction)endScrubbing:(id)sender; |
@end |
@implementation APLViewController |
{ |
@private |
BOOL _playing; |
BOOL _scrubInFlight; |
BOOL _seekToZeroBeforePlaying; |
float _lastScrubSliderValue; |
float _playRateToRestore; |
id _timeObserver; |
float _transitionDuration; |
NSInteger _transitionType; |
BOOL _transitionsEnabled; |
NSTimer *_progressTimer; |
} |
#pragma mark - View loading |
- (void)viewDidLoad |
{ |
[super viewDidLoad]; |
self.editor = [[APLSimpleEditor alloc] init]; |
self.clips = [[NSMutableArray alloc] initWithCapacity:2]; |
self.clipTimeRanges = [[NSMutableArray alloc] initWithCapacity:2]; |
// Defaults for the transition settings. |
_transitionType = kDiagonalWipeTransition; |
_transitionDuration = 2.0; |
_transitionsEnabled = YES; |
[self updateScrubber]; |
[self updateTimeLabel]; |
// Add the clips from the main bundle to create a composition using them |
[self setupEditingAndPlayback]; |
} |
- (void)viewDidAppear:(BOOL)animated |
{ |
[super viewDidAppear:animated]; |
if (!self.player) { |
_seekToZeroBeforePlaying = NO; |
self.player = [[AVPlayer alloc] init]; |
[self.player addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:(__bridge void *)(AVCustomEditPlayerViewControllerRateObservationContext)]; |
[self.playerView setPlayer:self.player]; |
} |
[self addTimeObserverToPlayer]; |
// Build AVComposition and AVVideoComposition objects for playback |
[self.editor buildCompositionObjectsForPlayback:YES]; |
[self synchronizePlayerWithEditor]; |
} |
- (void)viewWillDisappear:(BOOL)animated |
{ |
[super viewWillDisappear:animated]; |
[self.player pause]; |
[self removeTimeObserverFromPlayer]; |
} |
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender |
{ |
if ([segue.identifier isEqualToString:@"Transition"]) |
{ |
// Setup transition type picker controller before it is shown. |
APLTransitionTypeController *transitionTypePickerController = segue.destinationViewController; |
UIPopoverPresentationController *controller = transitionTypePickerController.popoverPresentationController; |
/* |
This will cause the 'adaptivePresentationStyleForPresentationController' and |
'viewControllerForAdaptivePresentationStyle' functions to be called. |
*/ |
controller.delegate = self; |
transitionTypePickerController.delegate = self; |
transitionTypePickerController.currentTransition = _transitionType; |
if (_transitionType == kCrossDissolveTransition) { |
// Make sure the view is loaded first |
if (!transitionTypePickerController.crossDissolveCell) |
[transitionTypePickerController loadView]; |
[transitionTypePickerController.crossDissolveCell setAccessoryType:UITableViewCellAccessoryCheckmark]; |
} else { |
// Make sure the view is loaded first |
if (!transitionTypePickerController.diagonalWipeCell) |
[transitionTypePickerController loadView]; |
[transitionTypePickerController.diagonalWipeCell setAccessoryType:UITableViewCellAccessoryCheckmark]; |
} |
} |
} |
// Called when the Set Transition view controller 'Done' button is pressed. |
- (void)doneAction:(id)sender |
{ |
// Dismiss the view controller that was presented. |
[self dismissViewControllerAnimated:YES completion:^{ |
//.. done |
}]; |
} |
// Specify the presentation style to use (called for iPhone only). |
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller |
{ |
return UIModalPresentationFullScreen; |
} |
/* |
Present/wrap the view controller in a navigation controller (for iPhone/compact). |
If this method is not implemented, or returns nil, then the originally presented view controller is used. |
*/ |
- (UIViewController *)presentationController:(UIPresentationController *)controller viewControllerForAdaptivePresentationStyle:(UIModalPresentationStyle)style |
{ |
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:controller.presentedViewController]; |
UIViewController *presentedViewController = controller.presentedViewController; |
if (presentedViewController != nil) |
{ |
presentedViewController.navigationItem.rightBarButtonItem = |
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone |
target:self |
action:@selector(doneAction:)]; |
} |
return navController; |
} |
#pragma mark - Simple Editor |
- (void)setupEditingAndPlayback |
{ |
AVURLAsset *asset1 = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"sample_clip1" ofType:@"m4v"]]]; |
AVURLAsset *asset2 = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"sample_clip2" ofType:@"mov"]]]; |
dispatch_group_t dispatchGroup = dispatch_group_create(); |
NSArray *assetKeysToLoadAndTest = @[@"tracks", @"duration", @"composable"]; |
[self loadAsset:asset1 withKeys:assetKeysToLoadAndTest usingDispatchGroup:dispatchGroup]; |
[self loadAsset:asset2 withKeys:assetKeysToLoadAndTest usingDispatchGroup:dispatchGroup]; |
// Wait until both assets are loaded |
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){ |
[self synchronizeWithEditor]; |
}); |
} |
- (void)loadAsset:(AVAsset *)asset withKeys:(NSArray *)assetKeysToLoad usingDispatchGroup:(dispatch_group_t)dispatchGroup |
{ |
dispatch_group_enter(dispatchGroup); |
[asset loadValuesAsynchronouslyForKeys:assetKeysToLoad completionHandler:^(){ |
// First test whether the values of each of the keys we need have been successfully loaded. |
for (NSString *key in assetKeysToLoad) { |
NSError *error; |
if ([asset statusOfValueForKey:key error:&error] == AVKeyValueStatusFailed) { |
NSLog(@"Key value loading failed for key:%@ with error: %@", key, error); |
goto bail; |
} |
} |
if (![asset isComposable]) { |
NSLog(@"Asset is not composable"); |
goto bail; |
} |
[self.clips addObject:asset]; |
// This code assumes that both assets are atleast 5 seconds long. |
[self.clipTimeRanges addObject:[NSValue valueWithCMTimeRange:CMTimeRangeMake(CMTimeMakeWithSeconds(0, 1), CMTimeMakeWithSeconds(5, 1))]]; |
bail: |
dispatch_group_leave(dispatchGroup); |
}]; |
} |
- (void)synchronizePlayerWithEditor |
{ |
if ( self.player == nil ) |
return; |
AVPlayerItem *playerItem = [self.editor playerItem]; |
if (self.playerItem != playerItem) { |
if ( self.playerItem ) { |
[self.playerItem removeObserver:self forKeyPath:@"status"]; |
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem]; |
} |
self.playerItem = playerItem; |
if ( self.playerItem ) { |
if ( [self.playerItem respondsToSelector:@selector(setSeekingWaitsForVideoCompositionRendering:)] ) |
self.playerItem.seekingWaitsForVideoCompositionRendering = YES; |
// Observe the player item "status" key to determine when it is ready to play |
[self.playerItem addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial) context:(__bridge void *)(AVCustomEditPlayerViewControllerStatusObservationContext)]; |
// When the player item has played to its end time we'll set a flag |
// so that the next time the play method is issued the player will |
// be reset to time zero first. |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem]; |
} |
[self.player replaceCurrentItemWithPlayerItem:playerItem]; |
} |
} |
- (void)synchronizeWithEditor |
{ |
// Clips |
[self synchronizeEditorClipsWithOurClips]; |
[self synchronizeEditorClipTimeRangesWithOurClipTimeRanges]; |
// Transitions |
if (_transitionsEnabled) { |
self.editor.transitionDuration = CMTimeMakeWithSeconds(_transitionDuration, 600); |
self.editor.transitionType = _transitionType; |
} else { |
self.editor.transitionDuration = kCMTimeInvalid; |
} |
// Build AVComposition and AVVideoComposition objects for playback |
[self.editor buildCompositionObjectsForPlayback:YES]; |
[self synchronizePlayerWithEditor]; |
} |
- (void)synchronizeEditorClipsWithOurClips |
{ |
NSMutableArray *validClips = [NSMutableArray arrayWithCapacity:2]; |
for (AVURLAsset *asset in self.clips) { |
if (![asset isKindOfClass:[NSNull class]]) { |
[validClips addObject:asset]; |
} |
} |
self.editor.clips = validClips; |
} |
- (void)synchronizeEditorClipTimeRangesWithOurClipTimeRanges |
{ |
NSMutableArray *validClipTimeRanges = [NSMutableArray arrayWithCapacity:2]; |
for (NSValue *timeRange in self.clipTimeRanges) { |
if (! [timeRange isKindOfClass:[NSNull class]]) { |
[validClipTimeRanges addObject:timeRange]; |
} |
} |
self.editor.clipTimeRanges = validClipTimeRanges; |
} |
#pragma mark - Utilities |
/* Update the scrubber and time label periodically. */ |
- (void)addTimeObserverToPlayer |
{ |
if (_timeObserver) |
return; |
if (self.player == nil) |
return; |
if (self.player.currentItem.status != AVPlayerItemStatusReadyToPlay) |
return; |
double duration = CMTimeGetSeconds([self playerItemDuration]); |
if (isfinite(duration)) { |
CGFloat width = CGRectGetWidth([self.scrubber bounds]); |
double interval = 0.5 * duration / width; |
/* The time label needs to update at least once per second. */ |
if (interval > 1.0) |
interval = 1.0; |
__weak APLViewController *weakSelf = self; |
_timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(interval, NSEC_PER_SEC) queue:dispatch_get_main_queue() usingBlock: |
^(CMTime time) { |
[weakSelf updateScrubber]; |
[weakSelf updateTimeLabel]; |
}]; |
} |
} |
- (void)removeTimeObserverFromPlayer |
{ |
if (_timeObserver) { |
[self.player removeTimeObserver:_timeObserver]; |
_timeObserver = nil; |
} |
} |
- (CMTime)playerItemDuration |
{ |
AVPlayerItem *playerItem = [self.player currentItem]; |
CMTime itemDuration = kCMTimeInvalid; |
if (playerItem.status == AVPlayerItemStatusReadyToPlay) { |
itemDuration = [playerItem duration]; |
} |
/* Will be kCMTimeInvalid if the item is not ready to play. */ |
return itemDuration; |
} |
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context |
{ |
if ( context == (__bridge void *)(AVCustomEditPlayerViewControllerRateObservationContext) ) { |
float newRate = [[change objectForKey:NSKeyValueChangeNewKey] floatValue]; |
NSNumber *oldRateNum = [change objectForKey:NSKeyValueChangeOldKey]; |
if ( [oldRateNum isKindOfClass:[NSNumber class]] && newRate != [oldRateNum floatValue] ) { |
_playing = ((newRate != 0.f) || (_playRateToRestore != 0.f)); |
[self updatePlayPauseButton]; |
[self updateScrubber]; |
[self updateTimeLabel]; |
} |
} |
else if ( context == (__bridge void *)(AVCustomEditPlayerViewControllerStatusObservationContext) ) { |
AVPlayerItem *playerItem = (AVPlayerItem *)object; |
if (playerItem.status == AVPlayerItemStatusReadyToPlay) { |
/* Once the AVPlayerItem becomes ready to play, i.e. |
[playerItem status] == AVPlayerItemStatusReadyToPlay, |
its duration can be fetched from the item. */ |
[self addTimeObserverToPlayer]; |
} |
else if (playerItem.status == AVPlayerItemStatusFailed) { |
[self reportError:playerItem.error]; |
} |
} |
else { |
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; |
} |
} |
- (void)updatePlayPauseButton |
{ |
UIBarButtonSystemItem style = _playing ? UIBarButtonSystemItemPause : UIBarButtonSystemItemPlay; |
UIBarButtonItem *newPlayPauseButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:style target:self action:@selector(togglePlayPause:)]; |
NSMutableArray *items = [NSMutableArray arrayWithArray:self.toolbar.items]; |
[items replaceObjectAtIndex:[items indexOfObject:self.playPauseButton] withObject:newPlayPauseButton]; |
[self.toolbar setItems:items]; |
self.playPauseButton = newPlayPauseButton; |
} |
- (void)updateTimeLabel |
{ |
double seconds = CMTimeGetSeconds([self.player currentTime]); |
if (!isfinite(seconds)) { |
seconds = 0; |
} |
int secondsInt = round(seconds); |
int minutes = secondsInt/60; |
secondsInt -= minutes*60; |
self.currentTimeLabel.textColor = [UIColor colorWithWhite:1.0 alpha:1.0]; |
self.currentTimeLabel.textAlignment = NSTextAlignmentCenter; |
self.currentTimeLabel.text = [NSString stringWithFormat:@"%.2i:%.2i", minutes, secondsInt]; |
} |
- (void)updateScrubber |
{ |
double duration = CMTimeGetSeconds([self playerItemDuration]); |
if (isfinite(duration)) { |
double time = CMTimeGetSeconds([self.player currentTime]); |
[self.scrubber setValue:time / duration]; |
} |
else { |
[self.scrubber setValue:0.0]; |
} |
} |
- (void)updateProgress:(NSTimer*)timer |
{ |
AVAssetExportSession *session = (AVAssetExportSession *)[timer userInfo]; |
if (session.status == AVAssetExportSessionStatusExporting) { |
_exportProgressView.progress = session.progress; |
} |
} |
- (void)reportError:(NSError *)error |
{ |
dispatch_async(dispatch_get_main_queue(), ^{ |
if (error) { |
NSString *title = NSLocalizedString(@"An Error Occurred", nil); |
NSString *message = [error localizedDescription]; |
NSString *cancelButtonTitle = NSLocalizedString(@"OK", nil); |
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; |
// Create the action. |
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:cancelButtonTitle style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { |
NSLog(@"The simple alert's cancel action occured."); |
}]; |
// Add the action. |
[alertController addAction:cancelAction]; |
[self presentViewController:alertController animated:YES completion:nil]; |
} |
}); |
} |
#pragma mark - IBActions |
- (IBAction)togglePlayPause:(id)sender |
{ |
_playing = !_playing; |
if ( _playing ) { |
if ( _seekToZeroBeforePlaying ) { |
[self.player seekToTime:kCMTimeZero]; |
_seekToZeroBeforePlaying = NO; |
} |
[self.player play]; |
} |
else { |
[self.player pause]; |
} |
} |
- (IBAction)beginScrubbing:(id)sender |
{ |
_seekToZeroBeforePlaying = NO; |
_playRateToRestore = [self.player rate]; |
[self.player setRate:0.0]; |
[self removeTimeObserverFromPlayer]; |
} |
- (IBAction)scrub:(id)sender |
{ |
_lastScrubSliderValue = [self.scrubber value]; |
if ( ! _scrubInFlight ) |
[self scrubToSliderValue:_lastScrubSliderValue]; |
} |
- (void)scrubToSliderValue:(float)sliderValue |
{ |
double duration = CMTimeGetSeconds([self playerItemDuration]); |
if (isfinite(duration)) { |
CGFloat width = CGRectGetWidth([self.scrubber bounds]); |
double time = duration*sliderValue; |
double tolerance = 1.0f * duration / width; |
_scrubInFlight = YES; |
[self.player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC) |
toleranceBefore:CMTimeMakeWithSeconds(tolerance, NSEC_PER_SEC) |
toleranceAfter:CMTimeMakeWithSeconds(tolerance, NSEC_PER_SEC) |
completionHandler:^(BOOL finished) { |
_scrubInFlight = NO; |
[self updateTimeLabel]; |
}]; |
} |
} |
- (IBAction)endScrubbing:(id)sender |
{ |
if ( _scrubInFlight ) |
[self scrubToSliderValue:_lastScrubSliderValue]; |
[self addTimeObserverToPlayer]; |
[self.player setRate:_playRateToRestore]; |
_playRateToRestore = 0.f; |
} |
/* Called when the player item has played to its end time. */ |
- (void)playerItemDidReachEnd:(NSNotification *)notification |
{ |
// After the movie has played to its end time, seek back to time zero to play it again. |
_seekToZeroBeforePlaying = YES; |
} |
- (IBAction)handleTapGesture:(UITapGestureRecognizer *)tapGestureRecognizer |
{ |
self.toolbar.hidden = !self.toolbar.hidden; |
self.currentTimeLabel.hidden = !self.currentTimeLabel.hidden; |
} |
- (IBAction)exportToMovie:(id)sender |
{ |
_exportProgressView.hidden = NO; |
[self.player pause]; |
[self.playPauseButton setEnabled:NO]; |
[self.transitionButton setEnabled:NO]; |
[self.scrubber setEnabled:NO]; |
[self.exportButton setEnabled:NO]; |
[self.editor buildCompositionObjectsForPlayback:NO]; |
// Get the export session from the editor |
AVAssetExportSession *session = [self.editor assetExportSessionWithPreset:AVAssetExportPresetMediumQuality]; |
// Remove the file if it already exists |
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ExportedProject.mov"]; |
[[NSFileManager defaultManager] removeItemAtPath:filePath error:NULL]; |
/* |
If a preset that is not compatible with AVFileTypeQuickTimeMovie is used, one can use |
-[AVAssetExportSession supportedFileTypes] to obtain a supported file type for the output file and |
UTTypeCreatePreferredIdentifierForTag to obtain an appropriate path extension for the output file type. |
*/ |
session.outputURL = [NSURL fileURLWithPath:filePath]; |
session.outputFileType = AVFileTypeQuickTimeMovie; |
[session exportAsynchronouslyWithCompletionHandler:^{ |
dispatch_async(dispatch_get_main_queue(), ^{ |
[self exportCompleted:session]; |
}); |
}]; |
// Update progress view with export progress |
_progressTimer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(updateProgress:) userInfo:session repeats:YES]; |
[[NSRunLoop currentRunLoop] addTimer:_progressTimer forMode:NSDefaultRunLoopMode]; |
} |
- (void)exportCompleted:(AVAssetExportSession *)session |
{ |
_exportProgressView.hidden = YES; |
_currentTimeLabel.hidden = NO; |
NSURL *outputURL = session.outputURL; |
[_progressTimer invalidate]; |
_progressTimer = nil; |
if ( session.status != AVAssetExportSessionStatusCompleted ) { |
NSLog(@"exportSession error:%@", session.error); |
[self reportError:session.error]; |
} |
if ( session.status != AVAssetExportSessionStatusCompleted ) { |
return; |
} |
_exportProgressView.progress = 1.0; |
/* |
Save the exported movie to the camera roll. |
Check authorization status. |
*/ |
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { |
if ( status == PHAuthorizationStatusAuthorized ) { |
dispatch_async( dispatch_get_main_queue(), ^{ |
// Save the movie file to the photo library and cleanup. |
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ |
/* |
In iOS 9 and later, it's possible to move the file into the photo library without duplicating |
the file data. This avoids using double the disk space during save, which can make a difference |
on devices with limited free disk space. |
*/ |
PHAssetResourceCreationOptions *options = [[PHAssetResourceCreationOptions alloc] init]; |
options.shouldMoveFile = YES; |
PHAssetCreationRequest *creationRequest = [PHAssetCreationRequest creationRequestForAsset]; |
[creationRequest addResourceWithType:PHAssetResourceTypeVideo fileURL:outputURL options:options]; |
} completionHandler:^( BOOL success, NSError *error ) { |
if ( ! success ) { |
NSLog( @"Could not save movie to photo library due to error: %@", error ); |
} |
}]; |
} ); |
} |
else { |
dispatch_async( dispatch_get_main_queue(), ^{ |
NSString *message = NSLocalizedString( @"AVCustomEdit doesn't have permission to the photo library, please change privacy settings", @"Alert message when the user has denied access to the photo library" ); |
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"AVCustomEdit" message:message preferredStyle:UIAlertControllerStyleAlert]; |
[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"Alert OK button") style:UIAlertActionStyleCancel handler:nil]]; |
[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Settings", @"Alert button to open Settings") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { |
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; |
}]]; |
[self presentViewController:alertController animated:YES completion:nil]; |
} ); |
} |
}]; |
[self.player play]; |
[self.playPauseButton setEnabled:YES]; |
[self.transitionButton setEnabled:YES]; |
[self.scrubber setEnabled:YES]; |
[self.exportButton setEnabled:YES]; |
} |
- (IBAction)setTransition:(id)sender |
{ |
// Show the view controller as a popover (iPad) or as a modal view controller (iPhone / iPhone Plus). |
APLTransitionTypeController *contentVC = [self.storyboard instantiateViewControllerWithIdentifier:@"SetTransition"]; |
assert(contentVC != nil); |
contentVC.edgesForExtendedLayout = UIRectEdgeNone; |
contentVC.modalPresentationStyle = UIModalPresentationPopover; |
UIPopoverPresentationController *presentationController = contentVC.popoverPresentationController; |
// Display popover from the UIButton (sender) as the anchor. |
presentationController.sourceRect = [sender frame]; |
UIButton *button = (UIButton *)sender; |
presentationController.sourceView = button.superview; |
presentationController.permittedArrowDirections = UIPopoverArrowDirectionAny; |
/* |
Present content view controller in a compact screen so that it can be dismissed as a full screen |
view controller. |
*/ |
presentationController.delegate = self; |
// Present the view controller modally. |
[self presentViewController:contentVC animated:YES completion:^{ |
//.. done |
}]; |
} |
#pragma mark - Gesture recognizer delegate |
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch |
{ |
if (touch.view != self.playerView) { |
// Ignore touch on toolbar. |
return NO; |
} |
return YES; |
} |
#pragma mark - APLTransitionTypePickerDelegate |
- (void)transitionTypeController:(APLTransitionTypeController *)controller didPickTransitionType:(int)transitionType |
{ |
_transitionType = transitionType; |
// Let the editor know of the change in transition type. |
[self synchronizeWithEditor]; |
[self dismissViewControllerAnimated:YES completion:nil]; |
} |
@end |
Copyright © 2017 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2017-08-17