How to open web view on button click in SwiftUI? - ios

Here is my WebView
struct Webview: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: Context) -> WebviewController {
let webviewController = WebviewController()
let request = URLRequest(url: self.url, cachePolicy: .returnCacheDataElseLoad)
webviewController.webview.load(request)
return webviewController
}
func updateUIViewController(_ webviewController: WebviewController, context: Context) {
//
}
}
class WebviewController: UIViewController, WKNavigationDelegate {
lazy var webview: WKWebView = WKWebView()
lazy var progressbar: UIProgressView = UIProgressView()
deinit {
self.webview.removeObserver(self, forKeyPath: "estimatedProgress")
self.webview.scrollView.removeObserver(self, forKeyPath: "contentOffset")
}
override func viewDidLoad() {
super.viewDidLoad()
self.webview.navigationDelegate = self
self.view.addSubview(self.webview)
self.webview.frame = self.view.frame
self.webview.translatesAutoresizingMaskIntoConstraints = false
self.view.addConstraints([
self.webview.topAnchor.constraint(equalTo: self.view.topAnchor),
self.webview.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
self.webview.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.webview.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
self.webview.addSubview(self.progressbar)
self.setProgressBarPosition()
webview.scrollView.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)
self.progressbar.progress = 0.1
webview.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
}
func setProgressBarPosition() {
self.progressbar.translatesAutoresizingMaskIntoConstraints = false
self.webview.removeConstraints(self.webview.constraints)
self.webview.addConstraints([
self.progressbar.topAnchor.constraint(equalTo: self.webview.topAnchor, constant: self.webview.scrollView.contentOffset.y * -1),
self.progressbar.leadingAnchor.constraint(equalTo: self.webview.leadingAnchor),
self.progressbar.trailingAnchor.constraint(equalTo: self.webview.trailingAnchor),
])
}
// MARK: - Web view progress
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
switch keyPath {
case "estimatedProgress":
if self.webview.estimatedProgress >= 1.0 {
UIView.animate(withDuration: 0.3, animations: { () in
self.progressbar.alpha = 0.0
}, completion: { finished in
self.progressbar.setProgress(0.0, animated: false)
})
} else {
self.progressbar.isHidden = false
self.progressbar.alpha = 1.0
progressbar.setProgress(Float(self.webview.estimatedProgress), animated: true)
}
case "contentOffset":
self.setProgressBarPosition()
default:
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}
And here is my button in the Content view.
Button(action: {
Webview(url: URL(string:"https://www.google.com")!)
})
{
Image("go")
}
}
Actually, when I am clicking it, nothing happens but when I move it outside of the button, it opens automatically.
My question is, how to open WebView on the button click, I am very new to SwiftUI.
Thank you.

You are calling in the wrong way.
Use this
struct ContentView: View {
#State private var isShowingWebView: Bool = false
var body: some View {
Button(action: {
isShowingWebView = true
// Webview(url: URL(string:"https://www.google.com")!) <<-- Remove
})
{
Text("go")
}
.sheet(isPresented: $isShowingWebView) {
Webview(url: URL(string:"https://www.google.com")!)
}
}
}

Related

I want to add activity indicator to my avplayer when its buffering

private func startVideo() {
if let url = URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/VolkswagenGTIReview.mp4") {
player = AVPlayer(url: url)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
playerViewController.view.frame = avPlayerView.bounds
addChild(playerViewController)
avPlayerView.addSubview(playerViewController.view)
playerViewController.didMove(toParent: self)
player?.play()
}
}
need to add a activity loader whenever the video is buffering
You can get this working using the following code. Observing the KeyPath on the Player can make you achieve this.
var activityIndicator: UIActivityIndicatorView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if player != nil {
player.play()
player.addObserver(self, forKeyPath: "timeControlStatus", options: [.old, .new], context: nil)
}
}
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "timeControlStatus", let change = change, let newValue = change[NSKeyValueChangeKey.newKey] as? Int, let oldValue = change[NSKeyValueChangeKey.oldKey] as? Int {
if #available(iOS 10.0, *) {
let oldStatus = AVPlayer.TimeControlStatus(rawValue: oldValue)
let newStatus = AVPlayer.TimeControlStatus(rawValue: newValue)
if newStatus != oldStatus {
DispatchQueue.main.async {[weak self] in
if newStatus == .playing || newStatus == .paused {
self!.activityIndicator.stopAnimating()
} else {
self!.activityIndicator.startAnimating()
}
}
}
} else {
// Fallback on earlier versions
self.activityIndicator.stopAnimating()
}
}
}

Is there a delegate method that gets called before the UIDocumentInteractionController share sheet pop ups

Please can someone help.I would like to add an alert message once a user clicks on the share button on a pdf viewer, i'm using UIDocumentInteractionController to preview the pdf document. and i wanted to know if there are any delegate methods or functions that i can override, where i can add my alert before opening the sharing sheet?[![enter image description here][1]][1]
class ViewController: UIViewController {
var documentController : UIDocumentInteractionController!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction private func buttonTapped(_ sender: Any) {
guard let fileURL = Bundle.main.url(forResource: "infomation", withExtension: "pdf") else {return}
documentController = UIDocumentInteractionController.init(url: fileURL)
documentController.delegate = self
documentController.presentPreview(animated: true)
}
}
extension ViewController: UIDocumentInteractionControllerDelegate {
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return self
}
func documentInteractionControllerWillPresentOpenInMenu(_ controller: UIDocumentInteractionController) {
}
func documentInteractionControllerWillPresentOptionsMenu(_ controller: UIDocumentInteractionController) {
}
func documentInteractionController(_ controller: UIDocumentInteractionController, willBeginSendingToApplication application: String?) {
}
}
but none of them get called when i click on the share button, even though i have set the delegate to be my view controller.is there a way that i can do this ?
Explanation:
There isn't a way to do that without hacking it together by iterating all the sub-views and override the button's action and target.
One clean way is to create your own preview controller like shown below. If you add the QLPreviewController to UINavigationController, it will automatically create a bottom toolbar which you don't want. To work around this, you add it as a child controller of a regular UIViewController and create a custom navigation bar.
You can also use the UTI for the UIActivityItem to determine how the item can be shared. I've only implemented the preview and basic sharing capabilities, but the rest should be super easy to do, and at least it's more customizable than UIDocumentInteractionController since you have full control of everything.
Now for the code that does all of this:
//
// ViewController.swift
// CustomDocumentController
//
// Created by brandon on 2022-01-13.
//
import UIKit
import QuickLook
import MobileCoreServices
import UniformTypeIdentifiers
private class PreviewItem: NSObject, QLPreviewItem {
var previewItemURL: URL?
var previewItemTitle: String?
init(title: String, url: URL) {
super.init()
previewItemURL = url
previewItemTitle = title
}
}
class DocumentInteractionController: UIViewController {
private let url: URL
private lazy var navigationBar: UINavigationBar = {
let navigationBar = UINavigationBar()
let navigationItem = UINavigationItem(title: name ?? "Document")
navigationItem.hidesBackButton = true
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(onDone))
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(onShare)) //UIImage(systemName: "square.and.arrow.up")
navigationBar.pushItem(navigationItem, animated: false)
return navigationBar
}()
private lazy var previewController: QLPreviewController = {
let previewController = QLPreviewController()
previewController.title = name
previewController.dataSource = self
previewController.delegate = self
previewController.reloadData()
return previewController
}()
var uti: String? {
didSet {
previewController.reloadData()
}
}
var name: String? {
didSet {
previewController.title = name
navigationBar.topItem?.title = name
}
}
init(url: URL) {
self.url = url
super.init(nibName: nil, bundle: nil)
//super.init(rootViewController: self.previewController)
self.modalPresentationStyle = .fullScreen
name = (try? url.resourceValues(forKeys: [.localizedNameKey]))?.localizedName
uti = (try? url.resourceValues(forKeys: [.typeIdentifierKey]))?.typeIdentifier
if uti == nil {
if #available(iOS 15.0, *) {
uti = UTType.url.identifier
} else {
uti = String(kUTTypeURL)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
addChild(previewController)
previewController.didMove(toParent: self)
let separator = UIView()
separator.backgroundColor = .lightGray
view.addSubview(navigationBar)
view.addSubview(separator)
view.addSubview(previewController.view)
NSLayoutConstraint.activate([
navigationBar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
navigationBar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
separator.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
separator.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
separator.topAnchor.constraint(equalTo: navigationBar.bottomAnchor),
separator.heightAnchor.constraint(equalToConstant: 1.0 / UIScreen.main.scale),
previewController.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
previewController.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
previewController.view.topAnchor.constraint(equalTo: separator.bottomAnchor),
previewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
[navigationBar, separator, previewController.view].forEach({
$0?.translatesAutoresizingMaskIntoConstraints = false
})
navigationBar.barTintColor = previewController.view.backgroundColor
view.backgroundColor = previewController.view.backgroundColor
}
}
extension DocumentInteractionController: QLPreviewControllerDelegate, QLPreviewControllerDataSource {
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return PreviewItem(title: name ?? "Document", url: url)
}
}
extension DocumentInteractionController {
#objc
private func onDone() {
self.dismiss(animated: true, completion: nil)
}
#objc
private func onShare() {
let activityViewController = UIActivityViewController(activityItems: [url],
applicationActivities: nil)
activityViewController.excludedActivityTypes = [.assignToContact]
if UIDevice.current.userInterfaceIdiom == .pad {
activityViewController.popoverPresentationController?.sourceView = self.view
}
self.present(activityViewController, animated: true, completion: nil)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let url = Bundle.main.url(forResource: "Testing", withExtension: ".md")
let doc = DocumentInteractionController(url: url!)
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.present(doc, animated: true, completion: nil)
}
}
}
Screenshots:

An observer was overreleased or smashed

I got a crash on first time launch the app
KVO_IS_RETAINING_ALL_OBSERVERS_OF_THIS_OBJECT_IF_IT_CRASHES_AN_OBSERVER_WAS_OVERRELEASED_OR_SMASHED
But the second time when I open the app it is not crash
Firebase Crashlytics Log:
Crashed: com.apple.main-thread
0 libobjc.A.dylib 0x1fa2d7dac object_isClass + 16
1 Foundation 0x1fbb7fbd8 KVO_IS_RETAINING_ALL_OBSERVERS_OF_THIS_OBJECT_IF_IT_CRASHES_AN_OBSERVER_WAS_OVERRELEASED_OR_SMASHED + 68
2 Foundation 0x1fbb7d36c NSKeyValueWillChangeWithPerThreadPendingNotifications.llvm.6024598272766318604 + 304
3 AVFoundation 0x201146074 __avplayeritem_fpItemNotificationCallback_block_invoke + 5800
4 libdispatch.dylib 0x1fab436c8 _dispatch_call_block_and_release + 24
5 libdispatch.dylib 0x1fab44484 _dispatch_client_callout + 16
6 libdispatch.dylib 0x1faaf09ec _dispatch_main_queue_callback_4CF$VARIANT$mp + 1068
7 CoreFoundation 0x1fb09a1bc __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
8 CoreFoundation 0x1fb095084 __CFRunLoopRun + 1964
9 CoreFoundation 0x1fb0945b8 CFRunLoopRunSpecific + 436
10 GraphicsServices 0x1fd308584 GSEventRunModal + 100
11 UIKitCore 0x227adb558 UIApplicationMain + 212
12 Boxit4me 0x102a59ca0 main (FAQVC.swift:29)
13 libdyld.dylib 0x1fab54b94 start + 4
How could I solve this crash?
Updates:
I think it is related to my SignupViewController:
import UIKit
import AVFoundation
import AVKit
import NVActivityIndicatorView
import Localize_Swift
import IQKeyboardManagerSwift
class BaseSignUpViewController: BaseViewController {
#IBOutlet weak var mainView: UIView!
#IBOutlet weak var alreadySignInLabel: UILabel!
#IBOutlet weak var businessAccountBtn: UIButton!
#IBOutlet weak var personalAccountBtn: UIButton!
#IBOutlet weak var signupAsLabel: UILabel!
#IBOutlet weak var videoView: UIView!
#IBOutlet weak var vwVideoContainer : UIView!
#IBOutlet weak var vwSignUpView : UIView!
#IBOutlet weak var btnSignUp : UIButton!
#IBOutlet weak var lblHowItWorks : UILabel!
#IBOutlet weak var btnFullScreenVideo: UIButton!
var player: AVPlayer?
var activityIndicator : NVActivityIndicatorView!
#IBOutlet var rapidLabel: IGUILabel!
#IBOutlet weak var thumbnailImage: UIImageView!
#IBOutlet weak var playVideoButton: UIButton!
class func initializeViewController() -> UINavigationController {
return (StoryBoard.SignUpFlow.storyboard().instantiateInitialViewController() as? UINavigationController)!
}
class func initializeViewBaseController() -> BaseSignUpViewController {
return StoryBoard.SignUpFlow.storyboard().instantiateViewController(withIdentifier: SegueIdentifiers.SignUpFlow.BaseSignUpViewController) as! BaseSignUpViewController
}
override func viewDidLoad() {
super.viewDidLoad()
IQKeyboardManager.shared.shouldResignOnTouchOutside = true
}
deinit {
NotificationCenter.default.removeObserver(self)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setUIText()
AppUtility.setUpNavigationBar()
self.navigationController?.setNavigationBarHidden(true, animated: true)
// self.navigationController?.isNavigationBarHidden = true
if (UserDefaults.standard.object(forKey: "LCLCurrentLanguageKey") as? String) == nil {
self.performSegue(withIdentifier: SegueIdentifiers.SignUpFlow.LanguageSelection, sender: nil)
}
self.btnFullScreenVideo.isHidden = true
self.thumbnailImage.isHidden = false
self.playVideoButton.isHidden = false
}
override func viewDidAppear(_ animated: Bool) {
self.showVideoView()
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player?.currentItem)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidAppear(animated)
}
override func setUIText() {
if(Localize.currentLanguage() == "ar"){
UIView.appearance().semanticContentAttribute = .forceRightToLeft
UINavigationBar.appearance().semanticContentAttribute = .forceRightToLeft
self.btnSignUp.semanticContentAttribute = .forceRightToLeft
self.btnSignUp.titleEdgeInsets = UIEdgeInsetsMake(0, -20, 0, 0)
self.thumbnailImage.image = getThumbnailImage(forUrl: URL(string: Constants.websiteBaseURL + "Content/Theme/videos/how_it_works_ar.mp4")!)
self.player = AVPlayer(url: URL(string: Constants.websiteBaseURL + "Content/Theme/videos/how_it_works_ar.mp4")!)
}else{
UIView.appearance().semanticContentAttribute = .forceLeftToRight
UINavigationBar.appearance().semanticContentAttribute = .forceLeftToRight
self.btnSignUp.semanticContentAttribute = .forceLeftToRight
self.btnSignUp.titleEdgeInsets = UIEdgeInsetsMake(0, 20, 0, 0)
self.thumbnailImage.image = getThumbnailImage(forUrl: URL(string: Constants.websiteBaseURL + "Content/Theme/videos/how_it_works.mp4")!)
self.player = AVPlayer(url: URL(string: Constants.websiteBaseURL + "Content/Theme/videos/how_it_works.mp4")!)
}
self.showVideoView()
self.btnSignUp.titleLabel?.font = Font.Regular.fontWithSize(21)
self.btnSignUp.setTitle("Signup As".localized(), for: .normal)
self.businessAccountBtn.setTitle("Premium Account".localized(), for: .normal)
self.personalAccountBtn.setTitle("Personal Account (Free)".localized(), for: .normal)
self.businessAccountBtn.titleLabel?.font = Font.Regular.fontWithSize(13) //UIFont().textFieldText()
self.personalAccountBtn.titleLabel?.font = Font.Regular.fontWithSize(13)
self.lblHowItWorks.text = "How it works".localized()
self.lblHowItWorks.font = Font.Regular.fontWithSize(20)
setupSignInLabel()
setupGestures()
setupMainView()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let objLanguagePreferenceVC = segue.destination as? LanguagePreferenceVC {
objLanguagePreferenceVC.languageChanged = {
self.setUIText()
}
}
if let objSignUp = segue.destination as? SignUpViewController {
if (sender as! UIButton) == self.personalAccountBtn {
objSignUp.accountType = AccountType.Personal
}
else{
objSignUp.accountType = AccountType.Premium
}
}
}
#IBAction func btnSignUpClicked(_ sender : UIButton){
self.performSegue(withIdentifier: SegueIdentifiers.SignUpFlow.SignUpSegue, sender: sender)
}
#IBAction func videoViewTapped(_ sender : UITapGestureRecognizer){
if self.player?.timeControlStatus == .playing {
self.player?.pause()
self.playVideoButton.isHidden = false
self.btnFullScreenVideo.isHidden = true
}
else{
self.player?.play()
self.btnFullScreenVideo.isHidden = false
self.playVideoButton.isHidden = true
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
self.player?.pause()
let playerItem = self.player?.currentItem
do{try self.removeObserver(self, forKeyPath: "actionAtItemEnd")}catch{}
do{try playerItem?.removeObserver(self, forKeyPath: "playbackBufferEmpty")}catch{}
do{try playerItem?.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")}catch{}
do{try playerItem?.removeObserver(self, forKeyPath: "playbackBufferFull") }catch{}
self.player = nil
}
func setupSignInLabel(){
let italicAttribute = [NSAttributedStringKey.font:Font.Regular.fontWithSize(13), NSAttributedStringKey.foregroundColor: Color.blueColor.value]
let nextDropString = NSAttributedString(string: "Already have an account?".localized(), attributes: italicAttribute)
let colorFontAttributes = [NSAttributedStringKey.font:Font.Regular.fontWithSize(13) , NSAttributedStringKey.foregroundColor: Color.theme.value]
let timerString = NSAttributedString(string: " " + "Login".localized(), attributes: colorFontAttributes)
let labelString = NSMutableAttributedString(attributedString: nextDropString)
labelString.append(timerString)
self.alreadySignInLabel.attributedText = labelString
}
func setupGestures() {
self.alreadySignInLabel.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(loadSignIn(_:)))
self.alreadySignInLabel.addGestureRecognizer(tapGesture)
}
func setupMainView(){
self.mainView.layer.masksToBounds = false;
self.mainView.layer.shadowOffset = CGSize.init(width: 0, height: 0)
self.mainView.layer.shadowOpacity = 0.4;
self.mainView.layer.shadowColor = UIColor.lightGray.cgColor
self.vwSignUpView.layer.masksToBounds = false;
self.vwSignUpView.layer.shadowOffset = CGSize.init(width: 0, height: 0)
self.vwSignUpView.layer.shadowOpacity = 0.4;
self.vwSignUpView.layer.shadowColor = UIColor.lightGray.cgColor
self.vwVideoContainer.layer.borderColor = UIColor.gray.withAlphaComponent(0.8).cgColor
self.vwVideoContainer.layer.borderWidth = 0.5
self.vwVideoContainer.layer.masksToBounds = false;
self.vwVideoContainer.layer.shadowOffset = CGSize.init(width: 0, height: 0)
self.vwVideoContainer.layer.shadowOpacity = 0.1;
self.vwVideoContainer.layer.shadowColor = UIColor.lightGray.cgColor
}
func showVideoView(){
addObserver(self, forKeyPath: "actionAtItemEnd", options: [], context: nil)
let playerItem = self.player?.currentItem
playerItem?.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil)
playerItem?.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil)
playerItem?.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil)
// create a video layer for the player
let layer: AVPlayerLayer = AVPlayerLayer(player: player)
layer.backgroundColor = UIColor.clear.cgColor //Color.custom(hexString: "#F9F9F9", alpha: 1.0).value.cgColor
// make the layer the same size as the container view
layer.frame = videoView.bounds
let xAxis = self.videoView.center.x
let yAxis = self.videoView.center.y
let frame = CGRect(x: (xAxis - 23), y: (yAxis - 23), width: 45, height: 45)
activityIndicator = NVActivityIndicatorView(frame: frame)
activityIndicator.type = .ballPulse // add your type
activityIndicator.color = Color.theme.value// add your color
// make the video fill the layer as much as possible while keeping its aspect size
layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
// add the layer to the container view
videoView.layer.addSublayer(layer)
self.videoView.addSubview(activityIndicator)
}
#objc func loadSignIn(_ sender : Any){
//self.goBack()
let businessSignupVC = SignInViewController(nibName: "SignInViewController", bundle: nil)
self.navigationController?.pushViewController(businessSignupVC, animated: true)
}
#IBAction func businessAccountBtnPressed(_ sender: Any) {
let businessSignupVC = BussninessSignupViewController(nibName: "BussninessSignupViewController", bundle: nil)
self.navigationController?.pushViewController(businessSignupVC, animated: true)
}
#IBAction func personalAccountBtnPressed(_ sender: Any) {
let personalSignupVC = PersonalSignupViewController(nibName: "PersonalSignupViewController", bundle: nil)
self.navigationController?.pushViewController(personalSignupVC, animated: true)
}
func getThumbnailImage(forUrl url: URL) -> UIImage? {
let asset: AVAsset = AVAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)
do {
let thumbnailImage = try imageGenerator.copyCGImage(at: CMTimeMake(32, 60) , actualTime: nil)
self.thumbnailImage.image = UIImage(cgImage: thumbnailImage)
return UIImage(cgImage: thumbnailImage)
} catch let error {
print(error)
}
return nil
}
#IBAction func btnFullScreenClicked(_ sender: UIButton) {
playVideo(needFullScreen:true)
}
#IBAction func playAction(_ sender: UIButton) {
sender.isHidden = true
playVideo()
// startLoadingView()
}
func playVideo(needFullScreen:Bool=false){
self.thumbnailImage.isHidden = true
self.playVideoButton.isHidden = true
self.btnFullScreenVideo.isHidden = false
if needFullScreen == false{
player?.play()
}else{
let playerViewController = AVPlayerViewController()
playerViewController.player = player
self.present(playerViewController, animated: true) {
playerViewController.player!.play()
}
}
}
// MARK: - Loop video when ended.
#objc func playerItemDidReachEnd(notification: NSNotification) {
self.player?.seek(to: kCMTimeZero)
self.player?.play()
}
// MARK: - Loop video when ended.
#objc func playerDidStarted(notification: NSNotification) {
// self.player?.seek(to: kCMTimeZero)
// self.player?.play()
}
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object is AVPlayerItem {
switch keyPath! {
case "playbackBufferEmpty":
startLoadingView()
print("playbackBufferEmpty")
case "playbackLikelyToKeepUp":
stopLoadingiew()
print("playbackLikelyToKeepUp")
case "playbackBufferFull":
stopLoadingiew()
print("playbackBufferFull")
default:
break
}
}
}
func startLoadingView(){
activityIndicator.color = Color.theme.value// add your color
self.videoView.addSubview(activityIndicator)
activityIndicator.startAnimating()
}
func stopLoadingiew(){
activityIndicator.stopAnimating()
activityIndicator.removeFromSuperview()
}
}
You call showVideoView every time setUIText is called, which adds several KVO observations:
addObserver(self, forKeyPath: "actionAtItemEnd", options: [], context: nil)
let playerItem = self.player?.currentItem
playerItem?.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil)
playerItem?.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil)
playerItem?.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil)
However, you only remove these in viewWillDisappear, which is not balanced. You must ensure that you only observe exactly one time for each property, and remove the observation exactly one time.
I followed Rob Napier answer and I made these 2 functions
var isObserversAdded = false
func addObservers(){
self.isObserversAdded = true
addObserver(self, forKeyPath: "actionAtItemEnd", options: [], context: nil)
let playerItem = self.player?.currentItem
playerItem?.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil)
playerItem?.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil)
playerItem?.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil)
}
func removeObservers(){
if isObserversAdded {
let playerItem = self.player?.currentItem
self.removeObserver(self, forKeyPath: "actionAtItemEnd")
playerItem?.removeObserver(self, forKeyPath: "playbackBufferEmpty")
playerItem?.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
playerItem?.removeObserver(self, forKeyPath: "playbackBufferFull")
self.isObserversAdded = false
}
}
so when I want to remove an observer I will be sure that I added it and I call removeObservers() every time before I call addObservers()

How do I dismiss a progressView when it's fully loaded?

I have a progress view that tracks the status of a Youtube video loading via WKWebView. When it's loaded, I want to dismiss the progressView because when I click back onto the main page of my app, the status bar is still there (but it doesn't display anything and takes up space, blocking other elements of my app). How do I, when the progressView is completely loaded, dismiss the progressView?
This is my class that deals with the WKWebView.
import UIKit
import WebKit
class VideoPlayViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
var progressView: UIProgressView!
var videoURL: String = ""
override func loadView() {
webView = WKWebView()
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: videoURL)!
webView.load(URLRequest(url: url))
webView.allowsBackForwardNavigationGestures = true
//Progress View/ Refresh Button
progressView = UIProgressView(progressViewStyle: .default)
progressView.sizeToFit()
let progressButton = UIBarButtonItem(customView: progressView)
progressView.progressTintColor = UIColor(red: 254, green: 53, blue: 98)
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let refresh = UIBarButtonItem(barButtonSystemItem: .refresh, target: webView, action: #selector(webView.reload))
refresh.tintColor = UIColor(red: 254, green: 53, blue: 98)
toolbarItems = [progressButton, spacer, refresh]
navigationController?.isToolbarHidden = false
// Lets the progress bar change according to what the Observer sends back (# from 0-1)
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "estimatedProgress" {
progressView.progress = Float(webView.estimatedProgress)
}
}
}
If you need more info / more code, let me know.
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let keyPath = keyPath else { return }
switch keyPath {
case "estimatedProgress":
// If you are using a `UIProgressView`, this is how you update the progress
progressView.isHidden = webView.estimatedProgress == 1
progressView.progress = Float(webView.estimatedProgress)
default:
break
}
}
and don't forgot to deinitialize the observers
deinit {
//remove all observers
webView.removeObserver(self, forKeyPath: "estimatedProgress")
}
Check webView(_:didFinish:) method of WKNavigationDelegate
You have to set typeable:
public typealias isCompletion = (_ isCompleted: Bool?) -> Void
And add variables like this in class:
var completion: isCompletion?
Now you have to create a method :
func webViewLoad(_ iscompleted:#escaping isCompletion){
completion = iscompleted
let url = URL(string: videoURL)!
webView.load(URLRequest(url: url))
webView.allowsBackForwardNavigationGestures = true
}
You have to set completion in the delegate method
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
print("finish to load")
completion(true)
}
You can call this method from viewdidload like this:
// show loading from here
webViewLoad { (flag) in
//hide loading here
}

Detect volume button press

Volume button notification function is not being called.
Code:
func listenVolumeButton(){
// Option #1
NSNotificationCenter.defaultCenter().addObserver(self, selector: "volumeChanged:", name: "AVSystemController_SystemVolumeDidChangeNotification", object: nil)
// Option #2
var audioSession = AVAudioSession()
audioSession.setActive(true, error: nil)
audioSession.addObserver(self, forKeyPath: "volumeChanged", options: NSKeyValueObservingOptions.New, context: nil)
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if keyPath == "volumeChanged"{
print("got in here")
}
}
func volumeChanged(notification: NSNotification){
print("got in here")
}
listenVolumeButton() is being called in viewWillAppear
The code is not getting to the print statement "got in here", in either case.
I am trying two different ways to do it, neither way is working.
I have followed this: Detect iPhone Volume Button Up Press?
Using the second method, the value of the key path should be "outputVolume". That is the property we are observing.
So change the code to,
var outputVolumeObserve: NSKeyValueObservation?
let audioSession = AVAudioSession.sharedInstance()
func listenVolumeButton() {
do {
try audioSession.setActive(true)
} catch {}
outputVolumeObserve = audioSession.observe(\.outputVolume) { (audioSession, changes) in
/// TODOs
}
}
The code above won't work in Swift 3, in that case, try this:
func listenVolumeButton() {
do {
try audioSession.setActive(true)
} catch {
print("some error")
}
audioSession.addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "outputVolume" {
print("got in here")
}
}
With this code you can listen whenever the user taps the volume hardware button.
class VolumeListener {
static let kVolumeKey = "volume"
static let shared = VolumeListener()
private let kAudioVolumeChangeReasonNotificationParameter = "AVSystemController_AudioVolumeChangeReasonNotificationParameter"
private let kAudioVolumeNotificationParameter = "AVSystemController_AudioVolumeNotificationParameter"
private let kExplicitVolumeChange = "ExplicitVolumeChange"
private let kSystemVolumeDidChangeNotificationName = NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification")
private var hasSetup = false
func start() {
guard !self.hasSetup else {
return
}
self.setup()
self.hasSetup = true
}
private func setup() {
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
return
}
let volumeView = MPVolumeView(frame: CGRect.zero)
volumeView.clipsToBounds = true
rootViewController.view.addSubview(volumeView)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.volumeChanged),
name: kSystemVolumeDidChangeNotificationName,
object: nil
)
volumeView.removeFromSuperview()
}
#objc func volumeChanged(_ notification: NSNotification) {
guard let userInfo = notification.userInfo,
let volume = userInfo[kAudioVolumeNotificationParameter] as? Float,
let changeReason = userInfo[kAudioVolumeChangeReasonNotificationParameter] as? String,
changeReason == kExplicitVolumeChange
else {
return
}
NotificationCenter.default.post(name: "volumeListenerUserDidInteractWithVolume", object: nil,
userInfo: [VolumeListener.kVolumeKey: volume])
}
}
And to listen you just need to add the observer:
NotificationCenter.default.addObserver(self, selector: #selector(self.userInteractedWithVolume),
name: "volumeListenerUserDidInteractWithVolume", object: nil)
You can access the volume value by checking the userInfo:
#objc private func userInteractedWithVolume(_ notification: Notification) {
guard let volume = notification.userInfo?[VolumeListener.kVolumeKey] as? Float else {
return
}
print("volume: \(volume)")
}
import AVFoundation
import MediaPlayer
override func viewDidLoad() {
super.viewDidLoad()
let volumeView = MPVolumeView(frame: CGRect.zero)
for subview in volumeView.subviews {
if let button = subview as? UIButton {
button.setImage(nil, for: .normal)
button.isEnabled = false
button.sizeToFit()
}
}
UIApplication.shared.windows.first?.addSubview(volumeView)
UIApplication.shared.windows.first?.sendSubview(toBack: volumeView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
AVAudioSession.sharedInstance().addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
do { try AVAudioSession.sharedInstance().setActive(true) }
catch { debugPrint("\(error)") }
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
AVAudioSession.sharedInstance().removeObserver(self, forKeyPath: "outputVolume")
do { try AVAudioSession.sharedInstance().setActive(false) }
catch { debugPrint("\(error)") }
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let key = keyPath else { return }
switch key {
case "outputVolume":
guard let dict = change, let temp = dict[NSKeyValueChangeKey.newKey] as? Float, temp != 0.5 else { return }
let systemSlider = MPVolumeView().subviews.first { (aView) -> Bool in
return NSStringFromClass(aView.classForCoder) == "MPVolumeSlider" ? true : false
} as? UISlider
systemSlider?.setValue(0.5, animated: false)
guard systemSlider != nil else { return }
debugPrint("Either volume button tapped.")
default:
break
}
}
When observing a new value, I set the system volume back to 0.5. This will probably anger users using music simultaneously, therefore I do not recommend my own answer in production.
If interested here is a RxSwift version.
func volumeRx() -> Observable<Void> {
Observable<Void>.create {
subscriber in
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(true)
} catch let e {
subscriber.onError(e)
}
let outputVolumeObserve = audioSession.observe(\.outputVolume) {
(audioSession, changes) in
subscriber.onNext(Void())
}
return Disposables.create {
outputVolumeObserve.invalidate()
}
}
}
Usage
volumeRx()
.subscribe(onNext: {
print("Volume changed")
}).disposed(by: disposeBag)

Resources