PKCanvas is not drawable as a subview of PDFKit OverlayView - ios

I want to overlay pencil kit canvases automatically on PDFView with the new PDFKit OverlayProvider Protocol in iPad OS 16.
I added PKCanvas view as a subview of OverlayView, which is UIView, for PDFKit. I succeed in displaying and deleting overlay views based on the OverlayViewProvider protocol.
Setup the pdfview in overlay provider
The canvases are displayed as it supposed to be, however, the pencil interaction is not activated. Instead, only the PDF UI scroll view gesture is activated. Should I drop PDF views' gestures?
PDFView for Swift UI
struct PDFKitViewRepresentable: UIViewRepresentable {
let pdfDocument: PDFDocument
let pdfView:PDFView
init(showing pdfDoc: PDFDocument, pdfView:PDFView) {
self.pdfDocument = pdfDoc
self.pdfView = pdfView
}
func makeUIView(context: Context) -> PDFView {
pdfView.displayDirection = .vertical
pdfView.displayMode = .singlePageContinuous
pdfView.usePageViewController(true)
pdfView.pageBreakMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
pdfView.autoScales = false
pdfView.delegate = context.coordinator
pdfView.pageOverlayViewProvider = context.coordinator
pdfView.backgroundColor = UIColor(white: 0.04, alpha: 0.2)
return pdfView
}
func updateUIView(_ pdfView: PDFView, context: Context) {
pdfView.document = pdfDocument
}
func makeCoordinator()-> Coordinator {
Coordinator()
}
}
Coordinator code
class Coordinator: NSObject, PDFPageOverlayViewProvider, PDFViewDelegate, PDFDocumentDelegate, UIGestureRecognizerDelegate, PKCanvasViewDelegate, PKToolPickerObserver {
var pageToViewMapping = [PDFPage: CustomUIView]()
func pdfView(_ view: PDFView, overlayViewFor page: PDFPage) -> UIView? {
var resultView:CustomUIView = CustomUIView()
if let overlayView = pageToViewMapping[page] {
var canvas = overlayView.viewWithTag((Int(page.label!)!))
canvas?.becomeFirstResponder()
resultView = overlayView
} else {
let toolPicker = PKToolPicker.init()
var canvasView = PKCanvasView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
resultView.backgroundColor = UIColor.red.withAlphaComponent(0.1)
canvasView.drawingPolicy = .anyInput
canvasView.backgroundColor = .yellow.withAlphaComponent(0.2)
resultView.frame = page.bounds(for: .mediaBox)
toolPicker.addObserver(canvasView)
toolPicker.setVisible(true, forFirstResponder: canvasView)
canvasView.delegate = resultView
canvasView.becomeFirstResponder()
canvasView.tag = Int(page.label!)!
resultView.addSubview(canvasView)
pageToViewMapping[page] = resultView
}
let page = MyPDFPage(page:page)
return resultView
}
func pdfView(_ pdfView: PDFView, willDisplayOverlayView overlayView: UIView, for page: PDFPage){
// Overlay view will display
}
func pdfView(_ pdfView: PDFView, willEndDisplayingOverlayView overlayView: UIView, for page: PDFPage){
// Overlay view would be disappear
}
}
In the 2022 WWDC PDFKit video - https://developer.apple.com/videos/play/wwdc2022/10089/ - it would be achievable with less than 30 lines of code. So I use the code snippet and check the basic mechanism. But can not draw on canvas.

The reason why drawing gestures and scrolling gestures are having conflict was configuration of PDF View.
pdfView.displayMode = .singlePageContinuous
pdfView.usePageViewController(false)
Then you should set isUserInteractionEnabled as true on overlay view install.
See PDFKit - PDFPageOverlayViewProvider with PKCanvasView is not forwarding Touch events - WWDC22 Session 10089

Related

How to cache contents of a WKWebView so my UICollectionView doesn't have lag when scrolling?

I have a UICollectionView with cells containing WKWebViews. When I scroll through these cells there is a lag because the WKWebViews aren't cached. Is there some way to cache the contents of the WKWebViews to prevent this lag?
For reference I am using the YouTubePlayer-Swift Cocoapod and this is my custom UICollectionViewCell:
import UIKit
import YouTubePlayer_Swift
class YouTubeVideoCollectionViewCell: UICollectionViewCell {
let borderView = UIView(frame: .zero)
var videoView = YouTubePlayerView(frame: .zero)
var video: YouTubeVideo? {
didSet {
UISetUp()
}
}
override open func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
self.updateConstraintsIfNeeded()
return super.preferredLayoutAttributesFitting(layoutAttributes)
}
override func layoutSubviews() {
super.layoutSubviews()
changeToShadowedCard()
self.contentView.layoutIfNeeded()
}
func changeToShadowedCard() {
backgroundColor = .clear
contentView.addSubview(borderView)
borderView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
borderView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
borderView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
borderView.rightAnchor.constraint(equalTo: self.contentView.rightAnchor),
borderView.leftAnchor.constraint(equalTo: self.contentView.leftAnchor),
])
borderView.layer.masksToBounds = true
borderView.backgroundColor = UIColor.secondarySystemBackground
borderView.layer.cornerRadius = 5
borderView.addShadow(shadowColor: UIColor.label.cgColor, shadowOffset: CGSize(width: 0, height: 0), shadowOpacity: 0.6, shadowRadius: 3)
}
func UISetUp() {
guard let id = video?.id else { return }
borderView.addSubview(videoView)
videoView.loadVideoID(id)
videoView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
videoView.topAnchor.constraint(equalTo: borderView.topAnchor),
videoView.bottomAnchor.constraint(equalTo: borderView.bottomAnchor),
videoView.rightAnchor.constraint(equalTo: borderView.rightAnchor),
videoView.leftAnchor.constraint(equalTo: borderView.leftAnchor),
])
videoView.clipsToBounds = true
videoView.layer.cornerRadius = 5
}
}
You are asking two different questions here.
The first one, about cache control of WKWebView - this question was asked and answered before I suggest you look for more info online. - here is one answer - Cache for WKWebView
About the package you are using to play youtube videos. First - it is using UIWebView and not WKWebView. Second im not sure about how Youtube caching works you should check it out. Because I don’t think you have caching for this streaming service

Firebaseui on iOS: can't set background color on customized subclassed login screen

I'm subclassing the login screen of Firebaseui with:
import UIKit
import FirebaseUI
class LoginViewControllerCustom: FUIAuthPickerViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .red
let arr = Bundle.main.loadNibNamed("LoginText", owner: nil)!
let v = arr[0] as! UIView
self.view.addSubview(v)
}
}
My implementation works as I see the xib LoginText loaded on login screen.
But the background color is royally ignored.
How to enforce a bg color on the login screen from that subclass?
Edit: if I apply the answer below with view.insertSubview(imageViewBackground, at: 0)
Here is what I get:
As you can see the image gets inserted under the view that holds the login button. If I set "at: 1" it completely cover the buttons and they can't be used.
I resolved the problem in an unexpected way.
On the delegate method that would load this controller, I changed:
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
return LoginViewControllerCustom(authUI: authUI)
}
to
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
return LoginViewControllerCustom(nibName: nil, bundle: Bundle.main, authUI: authUI)
}
The addition of Bundle.main solved the issue, and replaced the original controller by mine, which was several levels deeper until that.
Not sure exactly why, but this did solve the issue.
you can try to put "fake" image background:
override func viewDidLoad() {
super.viewDidLoad()
let width = UIScreen.main.bounds.size.width
let height = UIScreen.main.bounds.size.height
let imageViewBackground = UIImageView(frame: CGRect(x: 0, y: 0, width:
width, height: height))
imageViewBackground.backgroundColor = .red
view.insertSubview(imageViewBackground, at: 0)
let arr = Bundle.main.loadNibNamed("LoginText", owner: nil)!
let v = arr[0] as! UIView
self.view.addSubview(v)
}
Edit: try this it's not elegant but it solves the problem.
override func viewDidLoad() {
let scrollView = view.subviews[0]
scrollView.backgroundColor = .clear
let contentView = scrollView.subviews[0]
contentView.backgroundColor = .red
let width = UIScreen.main.bounds.size.width
let height = UIScreen.main.bounds.size.height
let backgroundImage = UIImageView(frame: CGRect(x: 0, y: -1, width: width, height: height))
view.backgroundColor = .red
backgroundImage.contentMode = UIView.ContentMode.scaleAspectFill
view.insertSubview(backgroundImage, at: 0)
}

Efficient off-screen UIView rendering and mirroring

I have a "off-screen" UIView hierarchy which I want render in different locations of my screen. In addition it should be possible to show only parts of this view hierarchy and should reflect all changes made to this hierarchy.
The difficulties:
The UIView method drawHierarchy(in:afterScreenUpdates:) always calls draw(_ rect:) and is therefore very inefficient for large hierarchies if you want to incorporate all changes to the view hierarchy. You would have to redraw it every screen update or observe all changing properties of all views. Draw view hierarchy documentation
The UIView method snapshotView(afterScreenUpdates:) also does not help much since I have not found a way to get a correct view hierarchy drawing if this hierarchy is "off-screen". Snapshot view documentation
"Off-Screen": The root view of this view hierarchy is not part of the UI of the app. It has no superview.
Below you can see a visual representation of my idea:
Here's how I would go about doing it. First, I would duplicate the view you are trying to duplicate. I wrote a little extension for this:
extension UIView {
func duplicate<T: UIView>() -> T {
return NSKeyedUnarchiver.unarchiveObject(with: NSKeyedArchiver.archivedData(withRootObject: self)) as! T
}
func copyProperties(fromView: UIView, recursive: Bool = true) {
contentMode = fromView.contentMode
tag = fromView.tag
backgroundColor = fromView.backgroundColor
tintColor = fromView.tintColor
layer.cornerRadius = fromView.layer.cornerRadius
layer.maskedCorners = fromView.layer.maskedCorners
layer.borderColor = fromView.layer.borderColor
layer.borderWidth = fromView.layer.borderWidth
layer.shadowOpacity = fromView.layer.shadowOpacity
layer.shadowRadius = fromView.layer.shadowRadius
layer.shadowPath = fromView.layer.shadowPath
layer.shadowColor = fromView.layer.shadowColor
layer.shadowOffset = fromView.layer.shadowOffset
clipsToBounds = fromView.clipsToBounds
layer.masksToBounds = fromView.layer.masksToBounds
mask = fromView.mask
layer.mask = fromView.layer.mask
alpha = fromView.alpha
isHidden = fromView.isHidden
if let gradientLayer = layer as? CAGradientLayer, let fromGradientLayer = fromView.layer as? CAGradientLayer {
gradientLayer.colors = fromGradientLayer.colors
gradientLayer.startPoint = fromGradientLayer.startPoint
gradientLayer.endPoint = fromGradientLayer.endPoint
gradientLayer.locations = fromGradientLayer.locations
gradientLayer.type = fromGradientLayer.type
}
if let imgView = self as? UIImageView, let fromImgView = fromView as? UIImageView {
imgView.tintColor = .clear
imgView.image = fromImgView.image?.withRenderingMode(fromImgView.image?.renderingMode ?? .automatic)
imgView.tintColor = fromImgView.tintColor
}
if let btn = self as? UIButton, let fromBtn = fromView as? UIButton {
btn.setImage(fromBtn.image(for: fromBtn.state), for: fromBtn.state)
}
if let textField = self as? UITextField, let fromTextField = fromView as? UITextField {
if let leftView = fromTextField.leftView {
textField.leftView = leftView.duplicate()
textField.leftView?.copyProperties(fromView: leftView)
}
if let rightView = fromTextField.rightView {
textField.rightView = rightView.duplicate()
textField.rightView?.copyProperties(fromView: rightView)
}
textField.attributedText = fromTextField.attributedText
textField.attributedPlaceholder = fromTextField.attributedPlaceholder
}
if let lbl = self as? UILabel, let fromLbl = fromView as? UILabel {
lbl.attributedText = fromLbl.attributedText
lbl.textAlignment = fromLbl.textAlignment
lbl.font = fromLbl.font
lbl.bounds = fromLbl.bounds
}
if recursive {
for (i, view) in subviews.enumerated() {
if i >= fromView.subviews.count {
break
}
view.copyProperties(fromView: fromView.subviews[i])
}
}
}
}
to use this extension, simply do
let duplicateView = originalView.duplicate()
duplicateView.copyProperties(fromView: originalView)
parentView.addSubview(duplicateView)
Then I would mask the duplicate view to only get the particular section that you want
let mask = UIView(frame: CGRect(x: 0, y: 0, width: yourNewWidth, height: yourNewHeight))
mask.backgroundColor = .black
duplicateView.mask = mask
finally, I would scale it to whatever size you want using CGAffineTransform
duplicateView.transform = CGAffineTransform(scaleX: xScale, y: yScale)
the copyProperties function should work well but you can change it if necessary to copy even more things from one view to another.
Good luck, let me know how it goes :)
I'd duplicate the content I wish to display and crop it as I want.
Let's say I have a ContentViewController which carries the view hierarchy I wish to replicate. I would encapsule all the changes that can be made to the hierarchy inside a ContentViewModel. Something like:
struct ContentViewModel {
let actionTitle: String?
let contentMessage: String?
// ...
}
class ContentViewController: UIViewController {
func display(_ viewModel: ContentViewModel) { /* ... */ }
}
With a ClippingView (or a simple UIScrollView) :
class ClippingView: UIView {
var contentOffset: CGPoint = .zero // a way to specify the part of the view you wish to display
var contentFrame: CGRect = .zero // the actual size of the clipped view
var clippedView: UIView?
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
clippedView?.frame = contentFrame
clippedView?.frame.origin = contentOffset
}
}
And a view controller container, I would crop each instance of my content and update all of them each time something happens :
class ContainerViewController: UIViewController {
let contentViewControllers: [ContentViewController] = // 3 in your case
override func viewDidLoad() {
super.viewDidLoad()
contentViewControllers.forEach { viewController in
addChil(viewController)
let clippingView = ClippingView()
clippingView.clippedView = viewController.view
clippingView.contentOffset = // ...
viewController.didMove(to: self)
}
}
func somethingChange() {
let newViewModel = ContentViewModel(...)
contentViewControllers.forEach { $0.display(newViewModel) }
}
}
Could this scenario work in your case ?

Add AVPlayer subview to UIView

I am using the following code to create a vertical scrollview with paging enabled which works perfectly fine. Now I would like to add a subview of the type AVPlayer (?) to homeView, view2, view3. I can put setupCustomPlayer() in the initiateScrollView() function which works very well, too. However, it is not attached to any of the previously-mentioned views. If I try to add a subview using the function addSubview(anything) it tells me a UIView is expected. I need the UIViews as the vertical sliding feed is very important. Does someone know how to add it as a subview? The video is going to cover the entire screen and going to serve as kind of a background video one could say (if you would like to know)
Code:
func setupCustomPlayer() {
print("check")
AVPlayerVC.view.frame = self.view.frame
AVPlayerVC.view.sizeToFit()
AVPlayerVC.showsPlaybackControls = false
self.view.addSubview(AVPlayerVC.view)
let videoURL = NSURL(string: "http://URL")
let player = AVPlayer(url: videoURL! as URL)
AVPlayerVC.player = player
AVPlayerVC.player?.play()
}
func initiateScrollView() {
//create scrollView with paging enabled
let scrollView = UIScrollView(frame: view.bounds)
scrollView.isPagingEnabled = true
view.addSubview(scrollView)
//do not show vertical scroll indicator
scrollView.showsVerticalScrollIndicator = false;
//get page size
let pageSize = view.bounds.size
//individual views
let homeView = UIView()
homeView.backgroundColor = .green
let view2 = UIView()
view2.backgroundColor = .blue
let view3 = UIView()
view3.backgroundColor = .red
//array with individual views
let pagesViews = [homeView, view2, view3]
//amount of views
let numberOfPages = pagesViews.count
//add subviews (pages)
for (pageIndex, page) in pagesViews.enumerated(){
page.frame = CGRect(origin: CGPoint(x:0 , y:CGFloat(pageIndex) * pageSize.height), size: pageSize)
scrollView.addSubview(page)
}
//define size of scrollView
scrollView.contentSize = CGSize(width: pageSize.width, height: pageSize.height * CGFloat(numberOfPages))
}
Could you try this?
func setupCustomPlayer() {
print("check")
let player = AVPlayer(url: yourFileUrl)
var playerLayer: AVPlayerLayer?
playerLayer = AVPlayerLayer(player: player)
playerLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
playerLayer!.frame = self.view.frame
self.view.layer.addSublayer(playerLayer!)
player.play()
}

Swift. Flip animation. FromView is not displayed

I've wrote this code to create a simple flip animation:
func frontView (view:UIView) ->UIView {
var frontView: UIView
frontView = UIView()
frontView.frame = view.frame
frontView.center = CGPoint(x: 0, y: 0)
return frontView
}
func backView (view:UIView) ->UIView {
var backView: UIView
backView = UIView()
backView.frame = view.frame
backView.backgroundColor = UIColor.redColor()
backView.center = CGPoint(x: view.frame.width/2, y: 0)
view.addSubview(backView)
return backView
}
func flipViewAnimation (viewToAnimate: UIView) {
var animationOption = self.animationOption
var duration = self.duration
UIView.transitionFromView(backView(viewToAnimate), toView: frontView(viewToAnimate), duration: duration, options: animationOption, completion: nil)
//
}
As a viewToAnimate I use views with labels and imageViews inside, which I've created in AutoLayout. The result I'm trying to achieve more or less should look like this. First I see views filled with color, then they flip and show the content inside (labels and ImageViews).
But it works in a different way. Views appear already with content (labels and ImageViews) then just flip and again show the same content.
I've created each view programmatically and it works very well for me now.

Resources