I want to play local video in AVPlayerViewController but did not find click event of Done button.
My video is able to play in AVPlayerViewController but I did not find next button , previous button and done button click event.
I've done this to get Done button click event from AVPlayerViewController.
First of all, Create an extension of Notification.Name like bellow
extension Notification.Name {
static let kAVPlayerViewControllerDismissingNotification = Notification.Name.init("dismissing")
}
Now, Create an extension of AVPlayerViewController and override viewWillDisappear like bellow
// create an extension of AVPlayerViewController
extension AVPlayerViewController {
// override 'viewWillDisappear'
open override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// now, check that this ViewController is dismissing
if self.isBeingDismissed == false {
return
}
// and then , post a simple notification and observe & handle it, where & when you need to.....
NotificationCenter.default.post(name: .kAVPlayerViewControllerDismissingNotification, object: nil)
}
}
THIS IS FOR ONLY BASIC FUNCTIONALITY THAT HANDLES DONE BUTTON'S EVENT.
happy coding...
Officially there is no Done Button click event, a engineer from Apple said about it here.
As for my research, I found out that there is one but really indirect way to get an event if Done Button was clicked.
I found out that the only variable of AVPlayerViewController, which is changing when done button is clicked is AVPlayerViewController.view.frame.
First of all view.frame is appearing in the center of the viewController.
If you present it with animated : true it goes to the bottom of viewController and the back to the center. When done is clicked it goes back to the bottom.
If you present it with animated : false there will be only 2 changes: frame will be at the center of viewController when you start to play video, and at the bottom, when Done is clicked.
So if you add observer to the AVPlayerViewController.view.frame in the callback to present(PlayerViewController, animated : true) you'll get only one call of the observer, right when done button is clicked and video view will be out of the screen.
In my case AVPlayerViewController was presented modally with animation. Code below worked for me:
Swift 3.0
override func viewDidLoad()
{
super.viewDidLoad()
let videoURL = NSURL(fileURLWithPath:Bundle.main.path(forResource: "MyVideo", ofType:"mp4")!)
let player = AVPlayer(url: videoURL as URL)
present(playerViewController, animated: false) { () -> Void in
player.play()
self.playerViewController.player = player
self.playerViewController.addObserver(self, forKeyPath: #keyPath(UIViewController.view.frame), options: [.old, .new], context: nil)
}}
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?)
{
print(self.playerViewController.view.frame)
//Player view is out of the screen and
//You can do your custom actions
}
Also, I found out when you click Done, AVPlayerViewController is not dismissed and you can see it in ParentViewController.presentedViewController, so you can't add observer to this property
Objective c version: (Based on vmchar answer)
- (void)viewDidLoad
{
[super viewDidLoad];
NSURL *url = [NSURL fileURLWithPath:path];
AVPlayer *player = [AVPlayer playerWithURL:url];
AVPlayerViewController *AVPlayerVc = [[AVPlayerViewController alloc] init];
if (AVPlayerVc) {
[self presentViewController:AVPlayerVc animated:YES completion:^{
[player play];
AVPlayerVc.player = player;
[AVPlayerVc addObserver:self forKeyPath:#"view.frame" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial) context:nil];
}];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:#"view.frame"]) {
CGRect newValue = [change[NSKeyValueChangeNewKey]CGRectValue];
CGFloat y = newValue.origin.y;
if (y != 0) {
NSLog(#"Video Closed");
}
}
}
This is a small improvement from the answer by #vmchar:
We can use the .isBeingDismissed method to ensure that the AVPlayerViewController is being closed instead of analysing the frame.
...
let videoURL = NSURL(fileURLWithPath:"video.mp4")
let player = AVPlayer(url: videoURL as URL)
present(playerViewController!, animated: false) { () -> Void in
player.play()
self.playerViewController!.player = player
self.playerViewController!.addObserver(self, forKeyPath:#keyPath(UIViewController.view.frame), options: [.old, .new], context: nil)
...
Then to observe the value
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?)
{
if (playerViewController!.isBeingDismissed) {
// Video was dismissed -> apply logic here
}
}
The easiest solution for me was to subclass AVPlayerViewController and add simple completion block to it.
class MYVideoController: AVPlayerViewController {
typealias DissmissBlock = () -> Void
var onDismiss: DissmissBlock?
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isBeingDismissed {
onDismiss?()
}
}
}
Usage
...
let avPlayerViewController = MYVideoController()
avPlayerViewController.onDismiss = { [weak self] in
print("dismiss")
}
I found a workaround without having to subclass the AVPlayerViewController, which the documentation clearly states you should not (https://developer.apple.com/documentation/avkit/avplayerviewcontroller). It is not pretty, but it got the job done for me by giving me somewhere to run code when the AVPlayerViewController is dismissed.
AnyViewController: UIViewController, AVPlayerViewControllerDelegate {
private let playerVC = AVPlayerViewController()
override func viewDidLoad() {
playerVC.delegate = self
}
func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
if playerViewController.isBeingDismissed {
playerViewController.dismiss(animated: false) { [weak self] in
self?.someDismissFunc()
}
}
}
}
It is really messed up that there is no clean way of doing this as far as I can tell :(
I had the same problem, and I found another trick (works with YoutubePlayer swift framework which plays videos in a webview, but you might be able to adapt it to other uses).
In my case, pressing the Done button and pressing the Pause button on the fullscreen video player both results in a pause event on the YoutubePlayer. However, as the video plays in a different window, I subclassed my application's main window and overrode the becomeKey and resignKey functions to store whether my window is the key window or not, like that:
class MyWindow:UIWindow {
override func becomeKey() {
Services.appData.myWindowIsKey = true
}
override func resignKey() {
Services.appData.myWindowIsKey = false
}
}
Once I have that, I can check whether my window is key when the video state goes to pause - when the Done button was pressed, my window is the key window and I can dismiss my video view controller, and in the Pause case my window is not the key window, so I do nothing.
I guess there are lots of ways to skin a cat. I needed to handle the 'Done' click event. In my case, I wanted to make sure I was able to hook into the event after the "X" was clicked and the AVPlayerViewController was COMPLETELY closed (dismissed).
Swift 4.0
protocol TableViewCellVideoPlayerViewControllerDelegate: class {
func viewControllerDismissed()
}
class MyPlayerViewController: AVPlayerViewController {
weak var navDelegate: TableViewCellVideoPlayerViewControllerDelegate?
open override func viewDidDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if self.isBeingDismissed {
self.navDelegate?.viewControllerDismissed()
}
}
}
class TodayCell: UITableViewCell, SFSafariViewControllerDelegate, TableViewCellVideoPlayerViewControllerDelegate {
func viewControllerDismissed() {
self.continueJourney()
}
}
You can look into this Stackoverflow post and here is a github's project for reference. You will have to use this :
self.showsPlaybackControls = true
Please also have a look into Apple's documentation
Related
I have a cell that takes up the entire screen so there is only 1 visible cell at a time. Inside the cell I have an AVPlayer. Inside the cell's parent vc I have a KVO observer that listens to the "timeControlStatus". When the player stops playing I call a function stopVideo() inside the cell to stop the player and either show a replay button or a play button etc.
3 problems:
1- When the video stops/reaches end if I don't use DispatchQueue inside the KVO the app crashes when the cell's function is called.
2- When the video stops and I do use DispatchQueue inside the KVO it's observer keeps observing and Xcode freezes (no crash). There is a print statement inside the KVO that prints indefinitely and that's why Xcode freezes. The only way to stop it is to kill Xcode otherwise the beachball of death keeps spinning.
3- In the KVO I tried to use a notification to send to the cell in place of calling the cell.stopVideo() function but the function inside the cell never runs.
How can i fix this issue?
It should be noted that outside of the KVO not working everything else works fine. I have a periodic time observer that runs perfectly for every cell whenever I scroll, the videos load fine, and when I press the cell stop/play the video it all works fine.
cell:
protocol MyCellDelegate: class {
func sendBackPlayerAndIndexPath(_ player: AVPlayer?, currentIndexPath: IndexPath?)
}
var player: AVPlayer?
var indexPath: IndexPath?
var playerItem: AVPlayerItem? {
didSet {
// add playerItem to player
delegate?.sendBackPlayerAndIndexPath(player, indexPath)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
player = AVPlayer()
// set everything else relating to the player
}
// both get initialized in cellForItem
var delegate: MyCellDelegate?
var myModel: MyModel? {
didSet {
let url = URL(string: myModel!.videUrlStr!)
asset = AVAsset(url: url)
playerItem = AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: ["playable"])
}
}
// tried using this with NotificationCenter but it didn't trigger from parent vc
#objc public func playVideo() {
if !player?.isPlaying {
player?.play()
}
// depending on certain conditions show a mute button, etc
}
// tried using this with NotificationCenter but it didn't trigger from parent vc
#objc public func stopVideo() {
player?.pause()
// depending on certain conditions show a reload button or a play button etc
}
parent vc
MyVC: ViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var player: AVPlayer?
var currentIndexPath: IndexPath?
var isObserving = false
func sendBackPlayerAndIndexPath(_ player: AVPlayer?, currentIndexPath: IndexPath?) {
if isObserving {
self.player?.removeObserver(self, forKeyPath: "status", context: nil)
self.player?.removeObserver(self, forKeyPath: "timeControlStatus", context: nil)
}
guard let p = player, let i = currentIndexPath else { return }
self.player = p
self.currentIndexPath = i
isObserving = true
self.player?.addObserver(self, forKeyPath: "status", options: [.old, .new], context: nil)
self.player?.addObserver(self, forKeyPath: "timeControlStatus", options: [.old, .new], context: nil)
}
// If I don't use DispatchQueue below the app crashes
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as AnyObject? === player {
if keyPath == "status" {
if player?.status == .readyToPlay {
DispatchQueue.main.async { [weak self] in
self?.playVideoInCell()
}
}
}
} else if keyPath == "timeControlStatus" {
if player?.timeControlStatus == .playing {
DispatchQueue.main.async { [weak self] in
self?.playVideoInCell()
}
} else {
print("3. Player is Not Playing *** ONCE STOPPED THIS PRINTS FOREVER and Xcode freezes but doesn't crash.\n")
DispatchQueue.main.async { [weak self] in
self?.stopVideoInCell()
}
}
}
}
}
func playVideoInCell() {
guard let indexPath = currentIndexPath else { return }
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }
cell.playVideo()
// also tried sending a NotificationCenter message to the cell but it didn't trigger
}
func stopVideoInCell() {
guard let indexPath = currentIndexPath else { return }
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }
cell.stopVideo()
// also tried sending a NotificationCenter message to the cell but it didn't trigger
}
In the comments #matt asked for the crash log (only occurs when not using DispatchQueue inside the KVO). I have zombies enabled and it didn't give me any info. The crash happens instantaneously and then just goes blank. It doesn't give me any info. I had to quickly take a screen shot just to get the pic otherwise it disappears right after the crash.
Inside the KVO, the 3rd one that prints forever, I removed the DispatchQueue and just added the stopVideoInCell() function. When the function calls the cell's stopVideo() function, player?.pause briefly gets a EXC_BAD_ACCESS (code=2, address=0x16b01ff0) crash.
The app then terminates. Inside the console the only thing that is printed is:
Message from debugger: The LLDB RPC server has crashed. The crash log
is located in ~/Library/Logs/DiagnosticReports and has a prefix
'lldb-rpc-server'. Please file a bug and attach the most recent crash
log
When I go to terminal and see what prints out the only thing I get is a bunch of lldb-rpc-server_2020-06-14-155514_myMacName.crash statements from all the days I ran into this crash.
When using DispatchQueue this doesn't occur and the video does stop but of course that print statement inside the KVO runs forever and Xcode freezes.
The problem is that, in your property observer, you are making a change in the property that you are observing. That is a vicious circle, an infinite recursion; Xcode displays this by freezing your app until ultimately you crash with (oh, the irony) a stack overflow.
Let's take a simpler, self-contained example. We have a UISwitch in the interface. It's On. If the user switches it Off, we want to detect that and switch it back to On. The example is a silly way to do this, but it perfectly illustrates the issue you are facing:
class ViewController: UIViewController {
#IBOutlet weak var theSwitch: UISwitch!
class SwitchHelper: NSObject {
#objc dynamic var switchState : Bool = true
}
let switchHelper = SwitchHelper()
var observer: NSKeyValueObservation!
override func viewDidLoad() {
super.viewDidLoad()
self.observer = self.switchHelper.observe(\.switchState, options: .new) {
helper, change in
self.theSwitch.isOn.toggle()
self.theSwitch.sendActions(for: .valueChanged)
}
}
#IBAction func doSwitch(_ sender: Any) {
self.switchHelper.switchState = (sender as! UISwitch).isOn
}
}
What's going to happen? The user turns the switch Off. We are observing that, as switchState; in response, we switch the switch back to On and call sendActions. And sendActions changes switchState. But we are still in the middle of the code that observes switchState! So we do it again and it happens again. And again and it happens again. Infinite loop...
How would you get out of this? You need to break the recursion somehow. I can think of two obvious ways. One is to think to yourself, "Well, I only care about a switch from On to Off. I don't care about the other way." Assuming that's true, you can solve the problem with a simple if, rather like the solution you elected to use:
self.observer = self.switchHelper.observe(\.switchState, options: .new) {
helper, change in
if let val = change.newValue, !val {
self.theSwitch.isOn.toggle()
self.theSwitch.sendActions(for: .valueChanged)
}
}
A more elaborate solution, which I like to use sometimes, is to stop observing when the observer is triggered, make whatever the change is, and then start observing again. You have to plan ahead a little to implement that, but it's sometimes worth it:
var observer: NSKeyValueObservation!
func startObserving() {
self.observer = self.switchHelper.observe(\.switchState, options: .new) {
helper, change in
self.observer?.invalidate()
self.observer = nil
self.theSwitch.isOn.toggle()
self.theSwitch.sendActions(for: .valueChanged)
self.startObserving()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.startObserving()
}
That looks recursive, because startObserving calls itself, but it isn't really, because what it does when it is called is to configure the observation; the code in the inner curly braces doesn't run until we get an observed change.
(In real life, I would probably make the NSKeyValueObservation a local variable in that configuration. But that's just an additional bit of elegance, not essential to the point of the example.)
I resolved this using a Boolean. It's not the most elegant answer but it works. If someone can come up with a better answer I'll accept it. It doesn't make sense what's going because of what I put under more:
answer:
var isPlayerStopped = false
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as AnyObject? === player {
if keyPath == "status" {
if player?.status == .readyToPlay {
DispatchQueue.main.async { [weak self] in
self?.playVideoInCell()
}
}
}
} else if keyPath == "timeControlStatus" {
if player?.timeControlStatus == .playing {
DispatchQueue.main.async { [weak self] in
self?.playVideoInCell()
}
} else {
if isPlayerStopped { return }
print("3. Player is Not Playing *** NOW THIS ONLY PRINTS ONCE.\n")
DispatchQueue.main.async { [weak self] in
self?.stopVideoInCell()
}
}
}
}
}
func playVideoInCell() {
guard let indexPath = currentIndexPath else { return }
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }
isPlayerStopped = false
cell.playVideo()
}
func stopVideoInCell() {
guard let indexPath = currentIndexPath else { return }
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return }
isPlayerStopped = true
cell.stopVideo()
}
more:
If I completely remove the DispatchQueues and the functions inside of them and use just print statements, the print statement that prints indefinitely print("3. Player is Not Playing... \n") only prints twice, it no longer prints indefinitely so I don't know what's going on with this.
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as AnyObject? === player {
if keyPath == "status" {
if player?.status == .readyToPlay {
print("1. Player is Playing\n")
}
} else if keyPath == "timeControlStatus" {
if player?.timeControlStatus == .playing {
print("2. Player is Playing\n")
} else {
print("3. Player is Not Playing... \n")
}
}
}
}
I am using AVPlayer to play online videos. It is working fine. Now the problem is, when I pop out from the view before the videos starts playing, the background process keeps running. And when the videos gets loaded, it starts playing in background.
Before dismiss view pause player
if player != nil {
player.pause()
player = nil
}
and also you need to pause player when your app goes to background
add this in your viewDidLoad
NotificationCenter.default.addObserver(self,
selector: #selector(applicationDidEnterBackground),
name: NSNotification.Name.UIApplicationDidEnterBackground,
object: nil)
add this in your viewController
#objc func applicationDidEnterBackground() {
if player != nil {
player.pause()
player = nil
}
}
By handling those both scenario you can stop player when you dismiss from that viewController also when your app goes into background
Hope this will help you
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if self.player != nil {
self.player.pause()
}
}
so here is my use case - we are loading a video using avplayer and playing it, and user can click on the default fullscreen button to take the video fullscreen. The user may be viewing the video in 2 different conditions, if he is logged in and if he is not logged in.
If the user is logged in (determined by a variables value), he can watch full video, otherwise, playback should stop after playing a defined number of seconds (depending on the video the no. of seconds changes), and a banner comes up over the player asking him to login.
Everything is working fine while the video is being played inline. However, when the video is playing fullscreen, then even if we stop playback using didTapPause(), the fullscreen window does not get dismissed. I have even tried dismissing it using self.playerController.dismiss(animated: true, completion: nil), the fullscreen modal is not dismissed. The code snippet it as follows -
playerController.player?.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, 1), queue: DispatchQueue.main) { (CMTime) -> Void in
if self.playerController.player?.currentItem?.status == .readyToPlay {
self.videoCurrentTimeDuration = CMTimeGetSeconds((self.playerController.player?.currentItem!.currentTime())!);
self.videoTimeDuration = CMTimeGetSeconds((self.playerController.player?.currentItem?.duration)!);
if self.moveToTime != nil{
let timeWithSecond = CMTimeMakeWithSeconds(self.videoTimeDuration! * self.moveToTime! / 100, Int32(kCMTimeMaxTimescale))
self.playerController.player?.seek(to: timeWithSecond, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)
self.moveToTime = nil
}
guard let videoD = self.videoData else { return }
let timeTToPlay: Double = Double(videoD.freeDuration)
if videoD.isFree {
if videoD.registrationNeeded && !CurrentLoginUser.shared.isLogin{
if self.videoCurrentTimeDuration! > timeTToPlay {
self.didTapPause()
self.playerController.dismiss(animated: true, completion: nil)
self.loginNeedView = UINib.get(withNib: self)
self.loginNeedView?.frame = self.bounds
self.loginNeedView?.autoresizingMask = [
UIViewAutoresizing.flexibleWidth,
UIViewAutoresizing.flexibleHeight
]
self.addSubview(self.loginNeedView!)
AppUtility.lockOrientation(UIInterfaceOrientationMask.portrait, andRotateTo: UIInterfaceOrientation.portrait)
}
}
else{
self.loginNeedView?.removeFromSuperview()
AppUtility.lockOrientation(UIInterfaceOrientationMask.all)
}
}
The player controller is added onto the view by calling the setupView function which is as follows -
private func setUpView() {
self.backgroundColor = .black
addVideoPlayerView()
configurateControls()
}
fileprivate func addVideoPlayerView() {
playerController.view.frame = self.bounds
playerController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
playerController.showsPlaybackControls = true
playerController.addObserver(self, forKeyPath: "videoBounds", options: NSKeyValueObservingOptions.new, context: nil)
self.insertSubview(playerController.view, at: 0)
}
I am not sure if this is the proper way to do it, any ideas ?
EDIT:
Next as per anbu.karthik's advice I tried to forcibly remove the fullscreen view by locating the top view controller as :
func currentTopViewController() -> UIViewController {
var topVC: UIViewController? = UIApplication.shared.delegate?.window??.rootViewController
while ((topVC?.presentedViewController) != nil) {
topVC = topVC?.presentedViewController
}
return topVC!
}
and then, used it as follows-
let currentTopVC: UIViewController? = self.currentTopViewController()
if (currentTopVC?.description.contains("AVFullScreenViewController"))! {
print("found it")
currentTopVC?.dismiss(animated: true) { _ in }
}
And it works but crashes the app with the following exception -
Terminating app due to uncaught exception 'UIViewControllerHierarchyInconsistency', reason: 'child view controller:<AVEmbeddedPlaybackControlsViewController: 0x7fdcc2264400> should have parent view controller:<AVPlayerViewController: 0x7fdcc222a800> but actual parent is:<AVFullScreenViewController: 0x7fdcce2f3c50>'
To my knowledge it is not possible to play full screen directly. It seems that AVPlayerViewController comes pretty much as is and does not offer much in the way of customization of UI or behavior. If you wanted to play full screen directly you would need to either present the controller containing your AVPlayerViewController modally in full screen or change the frame yourself to make it full screen. But there is no API to control full screen programmatically on AVPlayerViewController...
I am attempting to recognize a tap gesture while a video is playing so that I can dismiss it similarly to how snapchat does this, however, it says that MPMoviePlayerControllers have no members to add touch gestures, is this true or am I using the incorrect method?
var MP4 : NSData?
var MarkerLong : CLLocationDegrees?
var MarkerLat : CLLocationDegrees?
var Url : String?
var videoPlayer : MPMoviePlayerController!
private var firstAppear = true
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if firstAppear {
do {
try playVideo()
firstAppear = false
} catch AppError.InvalidResource(let name, let type) {
debugPrint("Could not find resource \(name).\(type)")
} catch {
debugPrint("Generic error")
}
}
}
private func playVideo() throws {
self.videoPlayer = MPMoviePlayerController()
self.videoPlayer.repeatMode = MPMovieRepeatMode.None
self.videoPlayer.contentURL = NSURL(string: Url!)
self.videoPlayer.controlStyle = MPMovieControlStyle.None
self.view.addSubview(self.videoPlayer.view)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(PlayVideoViewController.videoPlayBackDidFinish(_:)), name: MPMoviePlayerPlaybackDidFinishNotification, object: self.videoPlayer)
self.videoPlayer.view.frame.size = CGSizeMake(640, 1136)
self.videoPlayer.view.center = self.view.center
self.videoPlayer.play()
let gesture = UITapGestureRecognizer(target: self, action: "someAction:")
self.videoPlayer.addGestureRecognizer(gesture)
}
I would reccomend using AVPlayerViewController, but make sure not to subclass it, as Apple states not to.
1) MPMoviePlayer is deprecated (Don't use this code anymore)
2) AVPlayerViewController has a much more intricate set of code to allow more customization.
If you really want to customize something, you can subclass AVPlayer and make your own customized view where the video will be played, but you will have to add your own pause/start, etc...
MPMoviePlayerController is a view controller. Gesture recognizers are added to views. You need to add this gesture to the MPMoviePlayerController's view.
(Even better, stop using MPMoviePlayerController; it is deprecated.)
Goal: To take a screenshot of WKWebView after the website finished loading
Method employed:
Defined a WKWebView var in UIViewController
Created an extension method called screen capture() that takes image of WKWebView
Made my UIViewController to implement WKNavigationDelegate
Set the wkwebview.navigationDelegate = self ( in the UIViewController init)
Implemented the didFinishNavigation delegation func in UIViewcontroller to call screen capture extension method for WKWebView
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
let img = webView.screenCapture()
}
Questions:
When I debug i simulator, I notice that the control reaches the didFinishNavigation() func even though the website has not yet rendered in the WKWebView
Correspondingly the image screenshot taken is a white blob.
What am I missing here? I looked at all possible delegate functions for WKWebView and nothing else seem to represent the completion of content loading in WKWebView. Would appreciate help on if there is a work around
Update: Adding screenshot code that I am using to take a screenshot for web view
class func captureEntireUIWebViewImage(webView: WKWebView) -> UIImage? {
var webViewFrame = webView.scrollView.frame
if (webView.scrollView.contentSize != CGSize(width: 0,height: 0)){
webView.scrollView.frame = CGRectMake(webViewFrame.origin.x, webViewFrame.origin.y, webView.scrollView.contentSize.width, webView.scrollView.contentSize.height)
UIGraphicsBeginImageContextWithOptions(webView.scrollView.contentSize, webView.scrollView.opaque, 0)
webView.scrollView.layer.renderInContext(UIGraphicsGetCurrentContext())
var image:UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
webView.scrollView.frame = webViewFrame
return image
}
return nil
}
For those still looking for an answer to this, the marked answer is BS, he just forced his way into getting it accepted.
Using property,
"loading"
and
webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!)
both do the same thing, indicate if the main resource is loaded.
Now, that does not mean the entire webpage/website is loaded, because it really depends on the implementation of the website. If it needs to load scripts and resources (images, fonts etc) to make itself visible, you'll still see nothing after the navigation is completed, because the network calls made by the website are not tracked by the webview, only the navigation is tracked, so it wouldn't really know when the website loaded completely.
WKWebView doesn't use delegation to let you know when content loading is complete (that's why you can't find any delegate method that suits your purpose). The way to know whether a WKWebView is still loading is to use KVO (key-value observing) to watch its loading property. In this way, you receive a notification when loading changes from true to false.
Here's a looping animated gif showing what happens when I test this. I load a web view and respond to its loading property through KVO to take a snapshot. The upper view is the web view; the lower (squashed) view is the snapshot. As you can see, the snapshot does capture the loaded content:
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
if (self->_webKitView.isLoading == true) {
NSLog(#"Still loading...");
}else {
NSLog(#"Finished loading...");
[timer invalidate];
dispatch_async(dispatch_get_main_queue(), ^{
[self->_activityIndicator stopAnimating];
});
}
}];
Here is how I solved it:
class Myweb: WKWebView {
func setupWebView(link: String) {
let url = NSURL(string: link)
let request = NSURLRequest(URL: url!)
loadRequest(request)
addObserver(self, forKeyPath: "loading", options: .New, context: nil)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
guard let _ = object as? WKWebView else { return }
guard let keyPath = keyPath else { return }
guard let change = change else { return }
switch keyPath {
case "loading":
if let val = change[NSKeyValueChangeNewKey] as? Bool {
if val {
} else {
print(self.loading)
//do something!
}
}
default:break
}
}
deinit {
removeObserver(self, forKeyPath: "loading")
}
}
Update Swift 3.1
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let _ = object as? WKWebView else { return }
guard let keyPath = keyPath else { return }
guard let change = change else { return }
switch keyPath {
case "loading":
if let val = change[NSKeyValueChangeKey.newKey] as? Bool {
//do something!
}
default:
break
}
}
Lots of hand waiving in here, for incomplete solutions. The observer on "loading" is not reliable because when it's switched to NO, the layout hasn't happened yet and you can't get an accurate reading on page size.
The injection of JS into the view to report page size can also be problematic depending on actual page content.
The WKWebView uses, obviously, a scroller (a "WKWebScroller" subclass to be exact). So, your best bet is to monitor that scroller's contentSize.
- (void) viewDidLoad {
//...super etc
[self.webKitView.scrollView addObserver: self
forKeyPath: #"contentSize"
options: NSKeyValueObservingOptionNew
context: nil];
}
- (void) dealloc
{
// remove observer
[self.webKitView.scrollView removeObserver: self
forKeyPath: #"contentSize"];
}
- (void) observeValueForKeyPath: (NSString*) keyPath
ofObject: (id) object
change: (NSDictionary<NSKeyValueChangeKey,id>*) change
context: (void*) context
{
if ([keyPath isEqualToString: #"contentSize"])
{
UIScrollView* scroller = (id) object;
CGSize scrollFrame = scroller.contentSize;
NSLog(#"scrollFrame = {%#,%#}",
#(scrollFrame.width), #(scrollFrame.height));
}
}
Watch out for the contentSize: it gets triggered A LOT. If your webView is embedded into another scrolling area (like if you're using it as a form element) then when you scroll your entire view, that subview webView will trigger the changes for the same values. So, make sure you dont resize needlessly or cause needless refreshes.
This solution tested on iOS 12 on iPad Air 2 sim off High Sierra XCode 10.
It's not a good choice to check if the page content loaded from swift or objective-c especially for very complex page with many dynamic content.
A better way to inform you ios code from webpage's javascript. This make it very flexible and effective, you can notify ios code when a dom or the page is loaded.
You can check my post here for further info.
Most of these answers likely won't give you the results you're looking for.
Let the html document tell you when it's loaded.
Here is how it is done.
script message handler delegate
#interface MyClass : UIView <WKScriptMessageHandler>
Initialize the WKView to handle whatever event you'd like (e.g. window.load)
WKWebView* webView = yourWebView;
NSString* jScript = #"window.addEventListener('load', function () { window.webkit.messageHandlers.loadEvent.postMessage('loaded');})";
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
[webView.configuration.userContentController addScriptMessageHandler:self name:#"loadEvent"];
[webView.configuration.userContentController addUserScript:wkUScript];
Handle the delegate message.
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
NSString* name = message.name;
if([name compare:#"loadEvent"] == 0)
{
}
}
You can inject JavaScript into the web view to either wait for onDOMContentLoaded or check the document.readyState state. I have an app that has been doing this for years, and it was the only reliable way to wait for the DOM to be populated. If you need all the images and other resources to be loaded, then you need to wait for the load event using JavaScript. Here are some docs:
https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event
https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState