How to fix view controller hierarchy for AVPlayer? - ios

I have an iOS app with a single player and login. It should work like this:
1) Run app
2) login
3) after login video directly starts playing
4) if you exit the video it should ask for whether you want to logout
5) if no should continue to playing the stream
6) if yes logout the app
7) and if user logs in again without killing app it should repeat the steps from 2-7
However, my code does everything except the last step. If user logs out and logs in
- it starts playing the stream
but when you click the done button and try to exit from video, it does not work. Video stays full screen and it gives the following warning:
Warning: <AVPlayerViewController: 0x7f9fca08aa00> is trying to exit full screen, but is not in its view's window's view controller hierarchy. This results in undefined behavior.
I am really new to xcode ios and objectiveC and my code is as follows
- (void)viewDidLoad {
NSLog(#"ChannelListingViewController::viewDidLoad(): Called...");
[super viewDidLoad];
self.check=1;
NSLog(#"ChannelListingViewController::viewDidLoad(): Exiting...");
}
-(void) playVideo
{
self.channelURL = [TulixAppGlobalConfig getUserSubscribedPlanChannels];
NSURL *url = [[NSURL alloc] initWithString:[self.channelURL[1] getChannelHlsStreamURL]];
self.player = [AVPlayer playerWithURL:url];
// create a player view controller
[self presentViewController:controller animated:YES completion:nil];
self.controller.player = self.player;
[self.player play];
}
-(void) viewDidAppear:(BOOL)animated
{
self.controller = [[AVPlayerViewController alloc] init];
NSLog(#"ChannelListingViewController::viewDidAppear(): Called...");
if(self.check==0)
{
NSLog(#" 0 checkvalue: %i",check);
[self showLogoutConfirmationDialog];
}
else{
self.check =0;
NSLog(#" 1 checkvalue: %i",check);
[self playVideo];
}
[self spawnKeepAliveThread];
NSLog(#"ChannelListingViewController::viewDidAppear(): Exiting...");
}
-(void) showLogoutConfirmationDialog
{
void (^ptrFuncCompletionBlock)(void) = ^(void) { NSLog(#"ChannelListingViewController::showLogoutConfirmationDialog():GENERIC CALL BACK...");
[self attemptToLogoutUser];
};
NSLog(#"ChannelListingViewController::showLogoutConfirmationDialog(): Called...");
UIAlertAction* alertActionYes = [UIAlertAction actionWithTitle:#"Yes" style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
//Do Some action here
NSLog(#"ChannelListingViewController::showLogoutConfirmationDialog:: Called for YES...");
self.check = 1;
NSLog(#" yes checkvalue: %i",check);
[self.activeAlterController dismissViewControllerAnimated:NO completion:ptrFuncCompletionBlock];
[self attemptToLogoutUser];
}];
UIAlertAction* alertActionNo = [UIAlertAction actionWithTitle:#"No" style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {
NSLog(#"ChannelListingViewController::showLogoutConfirmationDialog:: Called for NO...");
[self playVideo];
}];
NSMutableArray* arrayAlertActions = [NSMutableArray arrayWithObjects:alertActionYes, alertActionNo, nil];
self.activeAlterController = [HelperUtility showMultiButtonDialogHavingTitle:#"Logout" withMessage:#"Are you sure you want to log out?" andAlertActions:arrayAlertActions];
[self presentViewController:self.activeAlterController animated:YES completion:nil];
NSLog(#"ChannelListingViewController::showLogoutConfirmationDialog(): Exit ");
}
I really don't understand why it does not work after the logout and need help.

You just need to relate your avplayer controller to window view. Instead just presenting the avcontroller add below line-
Objective C-
[self.view.window.rootViewController presentViewController:controller animated:YES completion:nil];
Swift 5 -
view.window.rootViewController?.present(controller, animated: true)

Related

MPMediaPickerController didPickMediaItems returns MPModelObjectMediaItem

I am having a super strange issue. I am using iOS13 MPMediaPickerController to get some songs from the users apple music library in order to play it via the [MPMusicPlayerController applicationMusicPlayer].
Now with some non-downloaded songs from some playlists that I have added to my library the delegate function
- (void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection
returns for
mediaItemCollection.items.firstObject
an object of the type MPModelObjectMediaItem which cannot be played by the player. (I can also not cast to that type because it's unknown. Also there is no documentation out there for this type. It seems to be a private class inside the mediaplayer framework.)
I am expecting the type MPConcreteMediaItem which is incoming for all downloaded (and all other cloud items that are not in playlists)
The picker is configured like this
musicPicker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeMusic];
musicPicker.showsCloudItems = true;
musicPicker.showsItemsWithProtectedAssets = true;
musicPicker.delegate = self;
musicPicker.allowsPickingMultipleItems = false;
Any tips whats going on there?
Well, it seems that items that you load into your own application cannot be properly loaded if you haven't added them into your music library. (iOS13.3)
I am using this workaround:
- (void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection
{
assert(mediaItemCollection.count == 1);
// this check is done because it seems that in the following scenario you dont get a
// MPConcreteMediaItem which can be played. Instead you get a MPModelObjectMediaItem
// this happens when an item was selected that is inside a playlist, but does not belong
// to your library (nor is it downloaded to your device)
if(mediaItemCollection.items.firstObject.persistentID <= 0)
{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:#"Song Not Added")
message:#"This song must be added to your Apple Music library. Please add the song to your library from the Music App."
preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:#"Dismiss"
style:UIAlertActionStyleDefault
handler:nil]];
[alertController addAction:[UIAlertAction actionWithTitle:#"Open Music App"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *alertAction)
{
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:#"music://"]
options:nil
completionHandler:nil];
}]];
[self presentViewController:alertController animated:true completion:nil];
return;
}
// good to go? then load it
[musicPlayer setQueueWithItemCollection:mediaItemCollection];
...
}

UIAlertController -- Capturing 'controller' strongly in this block

UIAlertController is crashing with this error message:
Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior (<UIAlertController: 0x7fb9107674d0>)
also with a warning is thrown trying to capture the textFields objectAtIndex.
Any ideas ?
Warning.. Capturing 'controller' strongly in this block is likely to lead to a retail cycle.
I also tried to create a #property (weak) reference the warning goes away but the app still crashes with this:
-(void)viewWillAppear:(BOOL)animated
{
// self.controller = [UIAlertController alloc];
UIAlertController* controller = [UIAlertController alertControllerWithTitle:#"Add Alergy To List" message:nil preferredStyle:(UIAlertControllerStyleAlert)];
[controller addTextFieldWithConfigurationHandler:^(UITextField * nametextField) {
_nameTextField.text = [controller.textFields objectAtIndex:0].text;
}];
UIAlertAction *save = [UIAlertAction actionWithTitle:#"Save Data" style:(UIAlertActionStyleDefault) handler:^(UIAlertAction * _Nonnull action) {
[self save:nil];
}];
UIAlertAction *cancel = [UIAlertAction actionWithTitle:#"Cancel Action" style:(UIAlertActionStyleCancel) handler:nil];
[controller addAction:save];
[controller addAction:cancel];
[self presentViewController:controller animated:YES completion:nil];
}
__weak __typeof(UIAlertController) weakController = controller;
__weak __typeof(UITextField) weakTextField = _nameTextField;
[controller addTextFieldWithConfigurationHandler:^(UITextField *nametextField){
// nameTextField is the textField being returned in the block
// if you want to use _nameTextField you need to make that weak too
nameTextField.text = [weakController.textFields objectAtIndex:0].text;
// or
weakTextField.text = [weakController.textFields objectAtIndex:0].text;
}];
Add
__weak __typeof(UIAlertController) weakController = controller;
before your addTextFieldWithConfigurationhandler:.
Then you should replace
_nameTextField.text = [controller.textFields objectAtIndex:0].text;
with
nameTextField.text = [weakController.textFields objectAtIndex:0].text;
Without the _.
_ is for internal access of properties. Here you have to update the field given in parameter
The fact is that you are in a block and if you want to get text from controller fields, you have to make a weak reference to it. The weak controller will prevent increase retain count.
You are trying to load alertcontroller before the viewcontroller is allocated this warning is because you are adding UIAlertController directly inside viewWillAppear
This method is called before the view controller's view is about to be added to a view hierarchy
Just give some time for view to get added to the view hierarchy.to do that use dispatch_after or some other delay functionality.
Since you are getting textfield input inside a block you can't use strong declaration,So you have to declare __weak typeof(UIAlertController) *weakcontroller = controller; before entering the block.Check apple developer site here for more understanding.
Overall modified code is given bekow:
-(void)viewWillAppear:(BOOL)animated{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
UIAlertController* controller = [UIAlertController alertControllerWithTitle:#"Add Alergy To List" message:nil preferredStyle:(UIAlertControllerStyleAlert)];
__weak typeof(UIAlertController) *weakcontroller = controller;
[controller addTextFieldWithConfigurationHandler:^(UITextField * nametextField) {
nametextField.text = [weakcontroller.textFields objectAtIndex:0].text;
}];
UIAlertAction *save = [UIAlertAction actionWithTitle:#"Save Data" style:(UIAlertActionStyleDefault) handler:^(UIAlertAction * _Nonnull action) {
[self save:nil];
}];
UIAlertAction *cancel = [UIAlertAction actionWithTitle:#"Cancel Action" style:(UIAlertActionStyleCancel) handler:nil];
[controller addAction:save];
[controller addAction:cancel];
[self presentViewController:controller animated:YES completion:nil];
});
}
It will work without any issues.

How can I programmatically dismiss a UIAlertController

I need to programmatically dismiss a UIAlertController that I'm using as a "please wait" message. I can present the alert without problem but when it comes to dismissing the alert, 50% of the time it dismisses and the other 50% it doesn't, forcing me to restart the app just to continue using it. Any ideas how to dismiss the alert with 100% consistency?
//loadingAlert is a UIAlertController declared in the .h file
//present the Alert
loadingAlert = [UIAlertController alertControllerWithTitle:#"Loading..." message:#"Please wait while we fetch locations" preferredStyle:UIAlertControllerStyleAlert];
[self presentViewController:loadingAlert animated:YES completion:nil];
//parse JSON file
_listOfAcquisitions = nil;
MNSHOW_NETWORK_ACTIVITY(YES);
NSString *WebServiceURL = [NSString stringWithFormat:#"JSON URL", _search];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSDictionary *dictionary = [JSONHelper loadJSONDataFromURL:WebServiceURL];
dispatch_async(dispatch_get_main_queue(), ^{
_listOfAcquisitions = [NSMutableArray array];
for (NSDictionary *oneEntry in dictionary) {
Acquisitions *acqu = [[Acquisitions alloc] init];
if([oneEntry objectForKey:#"ADDRESS1"] == (NSString *)[NSNull null]){acqu.ADDRESS1 = #"";}
else {acqu.ADDRESS1 = [oneEntry objectForKey:#"ADDRESS1"];}
if([oneEntry objectForKey:#"STATEABBR"] == (NSString *)[NSNull null]){acqu.STATEABBR = #"";}
else {acqu.STATEABBR = [oneEntry objectForKey:#"STATEABBR"];}
if([oneEntry objectForKey:#"TOWN"] == (NSString *)[NSNull null]){acqu.TOWN = #"";}
else {acqu.TOWN = [oneEntry objectForKey:#"TOWN"];}
if([oneEntry objectForKey:#"ZIPCODE"] == (NSString *)[NSNull null]){acqu.ZIPCODE = #"";}
else {acqu.ZIPCODE = [oneEntry objectForKey:#"ZIPCODE"];}
[_listOfAcquisitions addObject:acqu];
}
dispatch_async(dispatch_get_main_queue(), ^{
MNSHOW_NETWORK_ACTIVITY(NO);
[self refreshAnnotations:self];
});
});
});
//finally dismiss the alert...
[loadingAlert dismissViewControllerAnimated:YES completion:nil];
}
I've just been learning how to do this.
So, wherever the alert controller is built, you need to add the action button either to "OK" in default style or "Cancel" in cancel style.
UIAlertController *alertController = [UIAlertController
alertControllerWithTitle:#"You made a mistake."
message:#"Pray we don't alter the alert further"
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okayAction = [UIAlertAction
actionWithTitle:#"OK"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
[alertController dismissViewControllerAnimated:YES completion:nil];
}];
[alertController addAction:okayAction];
[self presentViewController:alertController animated:YES completion:nil];
There are other UIAlertActionStyle enumerations, such as UIAlertActionStyleCancel which will put a separator space between other actions and UIAlertActionStyleDestructive which will make the font red but will be in line with other UIAlertActions.
Make sure you add in order: standard actions (Okay, Open Camera, Photo Library) and THEN cancel actions.
There's also preferredStyle:UIAlertControllerStyleActionSheet which is used to set up options for the user. I use this to show Camera and Photo Library.
Also, for your specific dismiss action. The reason it's not working is because you are attempting to dismiss it on the background thread. You should ALWAYS dismiss or make UI changes on the foreground thread. This will cause an NSException/crash in the future.
dispatch_async(dispatch_get_main_queue(), ^{
// dismiss your UIAlertController
});
This is how you should be doing it with your specific code:
dispatch_async(dispatch_get_main_queue(), ^{
MNSHOW_NETWORK_ACTIVITY(NO);
[self refreshAnnotations:self];
[loadingAlert dismissViewControllerAnimated:YES completion:nil];
});
});
You have other issues with your code that you should ask others for help about. I am only a junior developer so I'm not sure how to correctly do what you're trying to do but this should help with dismissing.
You might want to look into loading wheels or toast messages that will say "Please wait" or "Loading".
Using a UIAlertController to show a loading message is rather bad taste.
First of all your network call should probably happen in the completion block of the presentViewController. So you don't dismiss it before it has appeared.
Also the nested dispatch_async seems off, since you call dispatch_get_main_queue(line 32) while already within the mainQue(line 13). And if this were to work the dismiss would need to be within the dispatch_async block so that it actually would dismiss.
But more importantly this is kind of a misuse of the UIAlertController API. Those are intended for user input not to Block UI.
You are better of implementing your own custom view subclass.
Or using MBProgressHUD (https://github.com/jdg/MBProgressHUD). There you can use either the MBProgressHUDModeIndeterminate or MBProgressHUDModeText to accomplish what you are trying.
You're creating/starting/dismissing the alert all within the same block, which means both presentViewController:loadingAlert and [loadingAlert dismissViewControllerAnimated: are being called in the same runloop. This is why you're getting unexpected results.
The dismiss needs to be called from a different cycle of the runloop, so having it called in a separate *block is what you want. You're already doing things in different threads using dispatch_async, which execute in discretely separate runloop so the solution for you is to put the dismissViewControllerAnimated: call within the dispatch_async(dispatch_get_main_queue() to ensure that its both called on the main thread for UI updates, and called in a separate run-loop as its presentation.
You could use dismissWithClickedButtonIndex:animated: but it's now deprecated in iOS9. I would use a timer:
// 3 second delay
[NSTimer scheduledTimerWithTimeInterval:3 target:self selector:#selector(dismissAlert) userInfo:nil repeats:NO];
- (void) dismissAlert {
[loadingAlert dismissWithClickedButtonIndex:0 animated:YES];
}
Of course, this isn't what UIAlertViews are intended for. You'd be better of using a loading spinner somewhere (UIActivityIndicatorView).

How do I present multiple UIAlertController one after another?

Inside of a forin loop, I need to present an UIAlertController and wait for user confirmation before presenting the next one. I presented them inside a forin loop, but only the first one appears(after confirmation, the others don't show up).Any help would be greatly appreciated.
You can use the UIAlertController delegate when a button is pressed you show the next alert.
Make a global alert index:
NSUInteger alertIndex = 0;
Make a global NSArray with your alert details in a NSDictionary eg:
self.alerts = #[#{#"title":#"Alert", #"message":#"Message 1"}, #{#"title":#"Alert", #"message":#"Message 2"}];
Call your first alert with an index, eg:
...title:self.alerts[alertIndex][#"title"]...
and
...message:self.alerts[alertIndex][#"message"]...
In your alert controller delegate's didClickButtonAtIndex:
alertIndex++;
// call your next alert here, explained above.
I need to do something similar to display hints to users throughout my app.
Just managed to cobble this together and it works quite well and is easy to adapt and add to ...
/////
// Alert Properties in #interface
////
#property NSOperationQueue *queue;
#property NSMutableArray* operationArray;
#property int operationCounter;
#property int operationTotal;
/////
// Alert Properties End
////
/////////
// Multi Alert Code Start
////////
- (void) alertCodeWithTitleString: (NSString*) titlestring AndMessageString:(NSString*)messagestring {
NSOperation *tempoperation = [NSBlockOperation blockOperationWithBlock: ^(void) {
NSString* tutTitleString = titlestring;
NSString* tutMessageString = messagestring;
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController * alert= [UIAlertController
alertControllerWithTitle:tutTitleString
message:tutMessageString
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* dontshow = [UIAlertAction
actionWithTitle:#"Don't Show This Again"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
[alert dismissViewControllerAnimated:YES completion:nil];
self.operationCounter++;
[self alertOperationCallMethod2];
}];
UIAlertAction* done = [UIAlertAction
actionWithTitle:#"Close"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
[alert dismissViewControllerAnimated:YES completion:nil];
self.operationCounter++;
[self alertOperationCallMethod2];
}];
[alert addAction:done];
[alert addAction:dontshow];
[self presentViewController:alert animated:YES completion:nil];
});
} ];
self.operationTotal++;
[self.operationArray addObject:tempoperation];
}
-(void) alertOperationCallMethod1 {
self.operationCounter = 0;
self.operationTotal = 0;
self.queue = [[NSOperationQueue alloc] init];
self.operationArray = [[NSMutableArray alloc] init];
[self alertCodeWithTitleString:#"Title1" AndMessageString:#"Message1"];
[self alertCodeWithTitleString:#"Title2" AndMessageString:#"Message2"];
[self alertCodeWithTitleString:#"Title3" AndMessageString:#"Message3"];
// Just keep adding method calls here to add alerts
[self alertOperationCallMethod2];
}
-(void) alertOperationCallMethod2 {
if (self.operationCounter<self.operationTotal) {
[self.queue addOperation:self.operationArray[self.operationCounter]];
}
}
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
//something here
[self alertOperationCallMethod1];
}
/////////
// Multi Alert Code End
////////
----
All you need to do is add another method call in alertOperationCallMethod1
Multiple versions of alertCodeWith..... method should allow you to customise your alerts to suit.
Hope this helps someone : )
This UIViewController extension does what you want:
The alerts queue up, fifo. I.e. latter alers wait and don't show until
user respond to former alerts.

UIImagePickerController not taking photos and freezes after I open second time

I have simple singleton class that takes care of creating and presenting UIImagePickerController. It worked just fine until few hours ago. All I did was moving the code from ViewController to separate singleton class (relevant code at the end of post). When I tried to undo all changes to get back to previous state, the problem persisted.
Whenever I present camera, I get warning (not sure if it is related or not)
"Snapshotting a view that has not been rendered results in an empty snapshot. Ensure your view has been rendered at least once before snapshotting or snapshot after screen updates."
Then, when I hit shutter button, nothing happens, no sound, no focus, no photo taken, no callback back to my app.
When I hit cancel, I get the callback and dismiss the controller without problems.
Now when I open camera again, the screen practically freezes, I get the controls and everything, but the screen doesn't update (actually it does, but only once or twice a minute)
#interface AddPhotoManager()
#property (nonatomic, weak) UIViewController *controller;
#property (nonatomic, strong) UIImagePickerController *imagePickerController;
#end
#implementation AddPhotoManager
+ (instancetype)sharedInstance
{
static dispatch_once_t once;
static id sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (void)addNewPhotoFromController:(UIViewController*)controller
{
self.controller = controller;
UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:#"Cancel" destructiveButtonTitle:nil otherButtonTitles:#"Camera", #"Photo library", nil];
[sheet showInView:self.controller.view];
}
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
if(buttonIndex == 0 || buttonIndex == 1) {
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
// Will get here on both iOS 7 & 8 even though camera permissions weren't required
// until iOS 8. So for iOS 7 permission will always be granted.
if (granted) {
// Permission has been granted. Use dispatch_async for any UI updating
// code because this block may be executed in a thread.
dispatch_async(dispatch_get_main_queue(), ^{
[self presentPickerController:buttonIndex == 0];
});
} else {
// Permission has been denied.
}
}];
}
}
- (void)presentPickerController:(bool)camera
{
self.imagePickerController = [[UIImagePickerController alloc] init];
self.imagePickerController.sourceType = camera ? UIImagePickerControllerSourceTypeCamera : UIImagePickerControllerSourceTypeSavedPhotosAlbum;
self.imagePickerController.delegate = self;
[self.controller presentViewController:self.imagePickerController animated:false completion:nil];
}
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
[picker dismissViewControllerAnimated:true completion:nil];
UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage];
UIImageWriteToSavedPhotosAlbum(image, self, nil, nil);
// TODO: upload the image
self.imagePickerController = nil;
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
[picker dismissViewControllerAnimated:true completion:nil];
self.imagePickerController = nil;
}
The singleton is used simply by calling
[[AddPhotoManager sharedInstance] addNewPhotoFromController:self];
from any view controller
EDIT:
storing imagePickerController in strong property doesn't seem to be necessary, but I wasn't sure so I tried it. However, results are same in both cases.
EDIT 2:
I tried exact same code in new test app and everything worked without problems, so it must be some interference with some other parts of the app? Any ideas what it could be? I tried deleting the app to reset permissions, even changed bundle Id, but nothing helped

Resources