I'm trying to integrate Apple Pencil with a WKWebView.
Desired behavior:
Using pencil allows you to draw on the webpage with all the fancy PencilKit integrations (PKToolPicker, etc)
Using fingers allows you to scroll through the webpage, and the scrolling is synced to the webview.
TAKE 2
So after spending a bit more time with this, I've arrived at an extremely hacky solution, and I don't actually like the solution:
Create a WKWebView and a transparent PKCanvasView on top of it.
Override the hitTest of the `PKCanvasView to always return nil.
Add the drawingGestureRecognizer of the PKCanvasView to the WKWebView
Make sure that drawingGestureRecognizer.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.pencil.rawValue)] is set for the PKCanvasView
This approach works but it removes a ton of flexibility for the implementation.
TAKE 1
So far I've tried two approaches:
Selectively cancel user input on the canvas when I detect it's coming from a finger. This didn't work because there was no way for me to detect this before the event was consumed by the view.
Create a transparant superview, and manually call touchesBegan/touchesMoved/touchesEnded for the PKCanvasView and WKWebView when I did my detection. Unfortunately, this didn't work either as calling those methods didn't do anything.
This is some basic code I have so far:
struct SUICanvasView: UIViewControllerRepresentable {
let url: URL?
func makeUIViewController(context: Context) -> ZRCanvasReader {
let canvasReader = ZRCanvasReader()
canvasReader.openUrl(url: url)
return canvasReader
}
func updateUIViewController(_ uiViewController: ZRCanvasReader, context: Context) {
}
typealias UIViewControllerType = ZRCanvasReader
}
class ZRCanvasReader: UIViewController {
lazy var canvas: PKCanvasView = {
let v = PKCanvasView()
v.isOpaque = false
v.backgroundColor = .clear
return v
}()
lazy var toolPicker: PKToolPicker = {
let toolPicker = PKToolPicker()
return toolPicker
}()
lazy var webView: WKWebView = {
let prefs = WKWebpagePreferences()
prefs.allowsContentJavaScript = true
let config = WKWebViewConfiguration()
config.defaultWebpagePreferences = prefs
let webview = WKWebView(frame: .zero, configuration: config)
return webview
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(webView)
view.addSubview(canvas)
webView.frame = view.frame
canvas.frame = view.frame
toolPicker.addObserver(canvas)
toolPicker.setVisible(true, forFirstResponder: canvas)
}
func openUrl(url: URL?) {
guard let loadingUrl = url else {
return
}
let request = URLRequest(url: loadingUrl)
webView.load(request)
}
}
I am building an application where I want to display a floor plan where the image is 1260x1000, larger than the size of my view controller. I want the user to be able to pan the image and zoom in and out, similar to how a map behaves in Mapview.
Below is the code in my view controller. When I run the simulator, the image is panning but the zooming in and out isn't working. Any suggestions on how to fix my code would be helpful.
class ViewController: UIViewController, UIScrollViewDelegate {
var scrollView: UIScrollView!
var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
imageView = UIImageView(image: UIImage(named: "myMap.pdf"))
scrollView = UIScrollView(frame: view.bounds)
scrollView.contentSize = imageView.bounds.size
scrollView.addSubview(imageView)
scrollView.delegate = self
scrollView.minimumZoomScale = 0.3
scrollView.maximumZoomScale = 5
view.addSubview(scrollView)
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return imageView
}
}
Your function signature is wrong:
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
Note: If you want to be able to scale your pdf image while keeping the vector-based rendering (so it doesn't get blurry when zoomed), you should probably use PDFKit and a PDFView.
Add your myMap.pdf file to your bundle... not to your Asset Catalog.
import UIKit
import PDFKit
class ZoomingPDFViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard let fileURL = Bundle.main.url(forResource: "myMap", withExtension: "pdf") else {
fatalError("Could not load myMap.pdf!")
}
// Add PDFView to view controller.
let pdfView = PDFView(frame: self.view.bounds)
pdfView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.view.addSubview(pdfView)
// Load myMap.pdf file from app bundle.
pdfView.document = PDFDocument(url: fileURL)
pdfView.autoScales = true
pdfView.maxScaleFactor = 5.0
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
}
}
When I open a safari view controller and then return to my app (when "Done" is pressed), my app renders a blank/white screen instead of the content for that view.
The code below is the code I have tried using in an empty view - the issue happens no matter where I try in my app.
import UIKit
import SafariServices
class SavedViewController: UIViewController, SFSafariViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if let link = URL(string: "https://google.com"){
let myrequest = SFSafariViewController(url: link)
myrequest.delegate = self
present(myrequest, animated:true)
}
}
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
dismiss(animated:true, completion: nil)
}
}
The website loads fine, its when I return to my app that the blank screen appears. Am I doing something wrong?
It works fine.
You just created new UIViewController this way, UIViewController by default have black background. So when you press Done you just come back from SafariViewController to you SavedViewController (UIViewController).
Probably You are looking for UIWebView solution https://developer.apple.com/documentation/uikit/uiwebview.
If u want only display SafariViewController do it as function from your ViewController, You don't need to create new File with UIViewController class to do it.
import UIKit
import SafariServices
class SavedViewController: UIViewController, SFSafariViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.backgroundColor = .gray
setupViews()
}
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
dismiss(animated:true, completion: nil)
}
private func setupViews(){
view.addSubview(openSafariButton)
openSafariButton.addTarget(self, action: #selector(handleOpenSafariButtonTap), for: .touchUpInside)
openSafariButton.frame = CGRect(x: 0, y: 100, width: view.frame.width, height: 60)
}
#objc func handleOpenSafariButtonTap(){
if let link = URL(string: "https://google.com"){
let myrequest = SFSafariViewController(url: link)
myrequest.delegate = self
present(myrequest, animated:true)
}
}
let openSafariButton: UIButton = {
let button = UIButton()
button.setTitle("openSafari", for: .normal)
button.setTitleColor(.black, for: .normal)
button.backgroundColor = .red
return button
}()
}
That is what i mean.
You can add function:
private func openSafariLink(link: String){
if let link = URL(string: link){
let myrequest = SFSafariViewController(url: link)
present(myrequest, animated:true)
}
}
And call it from any place like that:
openSafariLink(link: "https://google.com")
This way fits more for Your solution.
I have implemented a simple webview in ios with a PDF being displayed, and I want to do 2 things:
1- remove the gray padding area around the pdf doc when it's shown on webview
2-display a button in a specific location over the pdf file given the coordinates of the button
For both the two issues above, I have found code on SO and changed them to my needs. Here is the code I have so far (the view controller)
class DocAreaController: UIViewController, UIWebViewDelegate{
#IBOutlet var myWebView: UIWebView!
override func viewDidLoad() {
super.viewDidLoad()
myWebView.scrollView.scrollEnabled = false
myWebView.scrollView.bounces = false
myWebView.scrollView.pagingEnabled = true
myWebView.userInteractionEnabled = true
myWebView.delegate = self
func webViewDidStartLoad(myWebView: UIWebView){
let paddingScript = "document.body.style.margin='0';document.body.style.padding = '0'"
let result = myWebView.stringByEvaluatingJavaScriptFromString(paddingScript)
debugPrint("method called")
let btn: UIButton = UIButton(frame: CGRectMake(188, 340, 46, 30))
btn.backgroundColor = UIColor.cyanColor() //.colorWithAlphaComponent(0.5)
btn.userInteractionEnabled = false
btn.tag = 1 // change tag property
myWebView.addSubview(btn) // add to view as subview
}
// Get the document's file path.
let filepath = (NSBundle.mainBundle().pathForResource("Reader", ofType: "pdf"))! as String
// Create an NSURL object based on the file path.
let url = NSURL.fileURLWithPath(filepath)
// Create an NSURLRequest object.
let request = NSURLRequest(URL: url)
// Load the web viewer using the request object.
myWebView.loadRequest(request)
}
}
But the webViewDidStartLoad method is not getting called. I have set the delegate for webview both in code and storyboard. What m I missing?
Shift the webViewDidStartLoad out of viewDidLoad method. Place it after the viewDidLoad and it should work.
override func viewDidLoad() {
super.viewDidLoad()
myWebView.scrollView.scrollEnabled = false
myWebView.scrollView.bounces = false
myWebView.scrollView.pagingEnabled = true
myWebView.userInteractionEnabled = true
myWebView.delegate = self
// Get the document's file path.
let filepath = (NSBundle.mainBundle().pathForResource("Reader", ofType: "pdf"))! as String
// Create an NSURL object based on the file path.
let url = NSURL.fileURLWithPath(filepath)
// Create an NSURLRequest object.
let request = NSURLRequest(URL: url)
// Load the web viewer using the request object.
myWebView.loadRequest(request)
}
func webViewDidStartLoad(myWebView: UIWebView){
let paddingScript = "document.body.style.margin='0';document.body.style.padding = '0'"
let result = myWebView.stringByEvaluatingJavaScriptFromString(paddingScript)
debugPrint("method called")
let btn: UIButton = UIButton(frame: CGRectMake(188, 340, 46, 30))
btn.backgroundColor = UIColor.cyanColor() //.colorWithAlphaComponent(0.5)
btn.userInteractionEnabled = false
btn.tag = 1 // change tag property
myWebView.addSubview(btn) // add to view as subview
}
I have to draw a label or button on top of video relay next previous , leave comment . List of video have it, once user select one item from the table,it need to play, Once player play finished, those buttons or label should come on top of video
Here is my code :
comPlayerControl = AVPlayerViewController()
if let player = comPlayerControl {
let videoURL: String = "http://cdnapi.kaltura.com/p/11/sp/11/playManifest/entryId/"+selectedSubmission.transcodeRefId+"/format/applehttp/protocol/http/a.m3u8"
let playerItem = AVPlayerItem(URL: NSURL(string: videoURL)! )
commmentPlayer = AVPlayer(playerItem: playerItem)
player.player = commmentPlayer
player.view.frame = videoCell.frame
player.view.sizeToFit()
player.showsPlaybackControls = true
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(CommentsTableViewController.playerDidFinishPlaying(_:)),
name: AVPlayerItemDidPlayToEndTimeNotification,
object: playerItem
)
comPlayerControl.delegate = self
videoCell.addSubview(player.view)
}
func playerDidFinishPlaying(note: NSNotification) {
print("Video Finished")
let DynamicView=UIView(frame: CGRectMake(100, 200, 100, 100))
DynamicView.backgroundColor=UIColor.greenColor()
DynamicView.layer.cornerRadius=25
DynamicView.layer.borderWidth=2
DynamicView.layer.zPosition = 1;
comPlayerControl.view.addSubview(DynamicView)
}
requirement like this
You're using an AVPlayerViewController, so there's no reason to access your application's window like in Alessandro Ornano's answer. Why reinvent the wheel? Every AVPlayerViewController has a contentOverlayView property which allows you to place views between the player and the controls.
First, create a new AVPlayerItem and listen for the AVPlayerItemDidPlayToEndTimeNotification notification on that item. Load the item into your player and begin playback.
Once the item completes, the selector your specified to listen for the AVPlayerItemDidPlayToEndTimeNotification notification will be called. In that selector, access the contentOverlayView directly and add your buttons:
In some view controller or other object:
let playerVC = AVPlayerViewController()
// ...
func setupPlayer {
let playerItem = AVPlayerItem(...)
playerVC.player?.replaceCurrentItemWithPlayerItem(playerItem)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(VC.itemFinished), name: AVPlayerItemDidPlayToEndTimeNotification, object: playerItem)
self.presentViewController(playerVC, animated: true) {
self.playerVC.player?.play()
}
}
func itemFinished() {
let btn = UIButton(type: .System)
btn.addTarget(self, action: #selector(VC.buttonTapped), forControlEvents: .TouchUpInside)
self.playerVC.contentOverlayView?.addSubview(btn)
}
func buttonTapped() {
print("button was tapped")
// replay/comment logic here
}
As stated in the comments (and a rejected edit), buttons may not work in the contentOverlayView. For an alternate solution, see Pyro's answer.
You could also subclass AVPlayerViewController and do everything inside an instance of your subclass, but Apple warns against that:
Do not subclass AVPlayerViewController. Overriding this class’s methods is unsupported and results in undefined behavior.
I think the best way to make the avplayer buttons is explained here: IOS 8 Video Playback using AVPlayer and AVPlayerViewController .
So , I prefeer and agree with these instructions, but if you still want to
add these buttons you can try to add them to the self.window
if let app = UIApplication.sharedApplication().delegate as? AppDelegate, let window = app.window {
let myFirstButton = UIButton()
myFirstButton.setTitle("test", forState: .Normal)
window.addSubview(myFirstButton)
...
}
I recommend looking up AVPlayerLayer as a way to show buttons and other content on top of your video.
See the Advances in AVFoundation Playback WWDC presentation.
Also, check out the AVFoundationSimplePlayer-iOS example project.
Essentially, you create a view to host your player, and you make the layer behind the view into an AVPlayerLayer.
class PlayerView: UIView {
var player: AVPlayer? {
get {
return playerLayer.player
}
set {
playerLayer.player = newValue
}
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
}
Based on the your question and from the comment/code of Ramis i have made a sample code which you may try
As mentioned by JAL the contentOverlayView should be the best option to display the control over the video in the AVPlayerController, but as per my sample demo the contentOverlayView don't have any user interaction for the buttons or other controls, as if you check in the 3D view of the AVPlayerController it has AVTouchIgnoringView/UIView in front of the contentOverlayView which may be problem in user interaction with contentOverlayView.
So another solution is to add the overlay view in the AVPlayerViewController
func addContentOverlayView() {
OverlayView.frame = CGRectMake(0,30,AVPlayerVC.view.bounds.width, 100)
OverlayView.hidden = true
OverlayView.backgroundColor = UIColor ( red: 0.5, green: 0.5, blue: 0.5, alpha: 0.379 )
let btnNext = UIButton(frame:CGRectMake(AVPlayerVC.view.bounds.width - 60,0,60,44))
btnNext.setTitle(">>", forState:.Normal)
btnNext.addTarget(self, action:"playNext", forControlEvents:.TouchUpInside)
// btnNext.layer.borderColor = UIColor ( red: 0.0, green: 0.0, blue: 1.0, alpha: 0.670476140202703 ).CGColor
// btnNext.layer.borderWidth = 1.0
OverlayView.addSubview(btnNext)
let btnReplay = UIButton(frame:CGRectMake((AVPlayerVC.view.bounds.width/2)-40,0,80,44))
btnReplay.setTitle("Replay", forState:.Normal)
btnReplay.addTarget(self, action:"replayVideo", forControlEvents:.TouchUpInside)
OverlayView.addSubview(btnReplay)
let btnPrevious = UIButton(frame:CGRectMake(0,0,80,44))
btnPrevious.setTitle("<<", forState:.Normal)
btnPrevious.addTarget(self, action:"previousVideo", forControlEvents:.TouchUpInside)
OverlayView.addSubview(btnPrevious)
let btnComment = UIButton(frame:CGRectMake((AVPlayerVC.view.bounds.width/2)-70,40,140,44))
btnComment.setTitle("Comments", forState:.Normal)
btnComment.addTarget(self, action:"openComments", forControlEvents:.TouchUpInside)
OverlayView.addSubview(btnComment)
AVPlayerVC.view.addSubview(OverlayView);
}
func playNext() {
prevItem = AVPlayerVC.player?.currentItem
OverlayView.hidden = true
commmentQueuePlayer.advanceToNextItem()
}
func replayVideo() {
OverlayView.hidden = true
AVPlayerVC.player?.currentItem?.seekToTime(kCMTimeZero)
AVPlayerVC.player?.play()
}
func previousVideo() {
OverlayView.hidden = true
if prevItem != AVPlayerVC.player?.currentItem {
if (commmentQueuePlayer.canInsertItem(prevItem!, afterItem:AVPlayerVC.player?.currentItem)) {
//commmentQueuePlayer.insertItem(prevItem!, afterItem:AVPlayerVC.player?.currentItem)
commmentQueuePlayer.replaceCurrentItemWithPlayerItem(prevItem)
prevItem = AVPlayerVC.player?.currentItem
replayVideo()
}
} else {
replayVideo()
//Else display alert no prev video found
}
}
func stopedPlaying() {
if prevItem == nil {
prevItem = AVPlayerVC.player?.currentItem
}
OverlayView.hidden = false
}
At the initial setup we set the AVPlayerController,AVQueuePlayer etc... at that time we can add the overlay on the AVPlayerController
For the previous item there is no direct available and as per documentation the item will be remove once it's next item is played , so we have two option like replaceCurrentItemWithPlayerItem or insertItem(item: AVPlayerItem, afterItem: AVPlayerItem?)
If you need to check complete code you can check it from :
https://gist.github.com/Pyrolr/debb4fca8f608b1300e099a5b3547031
Note: This is just like prototype, it is not working perfectly in all the cases but it can help you in understanding the basic functionality you wnat and you can improve/optimise based on your requirements
Check Below code, I am able to add the button on overlay as well as button event is being called.
var playerViewController: AVPlayerViewController?
var OverlayView = UIView()
private lazy var button1: UIButton = {
let selfType = type(of: self)
let button = UIButton(frame: .init(origin: .zero, size: .init(width: selfType.musicWidth, height: selfType.musicHeight)))
button.setImage(UIImage(named: "star"), for: .normal)
button.addTarget(self, action: #selector(button1DidSelect), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: selfType.musicWidth),
button.heightAnchor.constraint(equalToConstant: selfType.musicHeight)
])
return button
}()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
guard let url = URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4") else { return }
let player = AVPlayer(url: url)
self.playerViewController = AVPlayerViewController()
guard let playerViewController = self.playerViewController else { return }
playerViewController.player = player
present(playerViewController, animated: true) {
playerViewController.player?.play()
}
self.addButtonsOnOverlayView()
self.playerViewController?.showsPlaybackControls = false
self.isHideOverlayControls = false
}
#objc private func button1DidSelect() {
print("button1 Selected")
}
private func addButtonsOnOverlayView() {
guard let overlayView = self.playerViewController?.contentOverlayView else { return }
if !self. button1.isDescendant(of: overlayView) {
overlayView.addSubview(self.musicFirstButton)
self.playerViewController?.showsPlaybackControls = true
}
NSLayoutConstraint.activate([
button1.leadingAnchor.constraint(equalTo: overlayView.leadingAnchor, constant: 20),
button1.topAnchor.constraint(equalTo: overlayView.topAnchor, constant: 20),
])
}