Custom 3D touch and WkWebView issue - ios

I am trying to implement a custom peek/pop behaviour in WkWebView.
I see the delegate being called and the controller being returned, but peek controller does not showup.
I can only get the peek controller to showup by setting
self.webView?.userInteractionEnabled = false
What could be going on here?
Code:
class ViewController: UIViewController, WKNavigationDelegate, UIViewControllerPreviewingDelegate {
var webView: WKWebView?
func webView(webView: WKWebView,
decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse,
decisionHandler: ((WKNavigationResponsePolicy) -> Void)){
decisionHandler(.Allow)
}
override func viewDidLoad() {
super.viewDidLoad()
/* Create our preferences on how the web page should be loaded */
let preferences = WKPreferences()
preferences.javaScriptEnabled = true
/* Create a configuration for our preferences */
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
/* Now instantiate the web view */
webView = WKWebView(frame: view.bounds, configuration: configuration)
if let theWebView = webView{
/* Load a web page into our web view */
let url = NSURL(string: "http://www.apple.com")
let urlRequest = NSURLRequest(URL: url!)
theWebView.loadRequest(urlRequest)
theWebView.navigationDelegate = self
view.addSubview(theWebView)
}
self.registerForPreviewingWithDelegate(self, sourceView: self.view)
}
func previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
self.webView?.userInteractionEnabled = false
let peekViewController = storyboard!.instantiateViewControllerWithIdentifier("peekaboo") as UIViewController
/*
Set the height of the preview by setting the preferred content size of the detail view controller.
Width should be zero, because it's not used in portrait.
*/
peekViewController.preferredContentSize = CGSize(width: 0.0, height: 0.0)
// Set the source rect to the cell frame, so surrounding elements are blurred.
//previewingContext.sourceRect = (self.webkitWebView?.frame)!
return peekViewController
}
/// Present the view controller for the "Pop" action.
func previewingContext(previewingContext: UIViewControllerPreviewing, commitViewController viewControllerToCommit: UIViewController) {
// Reuse the "Peek" view controller for presentation.
}
}

Set a prefferedContentSize height
peekViewController.preferredContentSize = CGSize(width: 0.0, height: 150)

Related

Overlaying PKCanvasView on top of WKWebView

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)
}
}

Change background color of status bar

I have a very simple web view project which just loads a website. There is a bug though, the website content is showing in the status bar if I scroll down.
I read that this is because the background color of the ViewController is set to be transparent, how can I change it to another color?
I tried it like this:
let color = UIColor.black;
self.view.backgroundColor = color
But nothing changes
Whole Code:
ViewController.swift
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
#IBOutlet var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
// 1 The webView loads the url using an URLRequest object.
let url = URL(string: "https://www.blizz-z.de/")!
webView.load(URLRequest(url: url))
// 2 A refresh item is added to the toolbar which will refresh the current webpage.
let refresh = UIBarButtonItem(
barButtonSystemItem: .refresh,
target: webView,
action: #selector(webView.reload)
)
toolbarItems = [refresh]
navigationController?.isToolbarHidden = true
navigationController?.isNavigationBarHidden = true
}
override func loadView() {
webView = WKWebView()
webView.navigationDelegate = self
view = webView
let color = UIColor.black;
self.view.backgroundColor = color
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
title = webView.title
}
}
I could just disable the status bar with the following code to fix the bug, but I try to keep it:
override var prefersStatusBarHidden: Bool {
return true
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let height = UIScreen.main.bounds.height
let width = UIScreen.main.bounds.width
let webView = WKWebView(frame: CGRect(x: 0, y: 0, width: width, height: height))
webView.load(URLRequest(url: (URL(string: "https://www.blizz-z.de/")!)))
view.addSubview(webView)
UIApplication.shared.statusBarView?.backgroundColor = #colorLiteral(red: 0.9529411765, green: 0.9529411765, blue: 0.9529411765, alpha: 1)
}
}
extension UIApplication {
var statusBarView: UIView? {
if responds(to: Selector("statusBar")) {
return value(forKey: "statusBar") as? UIView
}
return nil
}
}
Make the webView.top constraint relative to safeArea.top constraint, and not the superview.top constraint. This allows you to make your webView under the status bar.

Loading animation stucked/freezed when webview loads

I load the progress loading animation and when the response from Alamofire comes I use part of the response to construct the full url I need to load in the wkwebview and then I trigger webview.load(..).
My problem is that the progress loading animation gets stuck as soon as webview.load(..) starts to happen and remain stuck till I hide() it.
How can I actually have my animation to keep moving meanwhile the webview starts loading the page?
MyViewController.swift
class MyViewController: UIViewController, WKScriptMessageHandler {
var webView: WKWebView?
#IBOutlet weak var webViewContainer: UIView!
var webConfig:WKWebViewConfiguration {
get {
let webCfg:WKWebViewConfiguration = WKWebViewConfiguration()
let userController:WKUserContentController = WKUserContentController()
userController.add(self, name: "mycontroller")
webCfg.userContentController = userController;
return webCfg;
}
}
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView (frame: webViewContainer.bounds, configuration: webConfig)
webView!.autoresizingMask = [.flexibleWidth, .flexibleHeight]
webView?.scrollView.isScrollEnabled = false
webViewContainer.addSubview(webView!)
loadWebview()
}
func loadWebview(){
Loading.shared.show(self.view)
Alamofire.request(MYAPI, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: nil)
.responseJSON { response in
let url = URL(string: "https://path-to-load/\(response.key)")
self.webView!.load(URLRequest(url: url!))
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
let message = message.body as! [String:AnyObject]
let event = message["event"] as? String ?? "empty"
switch (event){
case "loading-finished":
DispatchQueue.main.async {
Loading.shared.hide(animated: true)
}
break
default:
break
}
}
}
Loading.swift
public class Loading {
var blurredEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
let imageView = UIImageView(image: UIImage(named: "loading_image"))
class var shared: Loading {
struct LoadingStatic {
static let instance: Loading = Loading()
}
return LoadingStatic.instance
}
init() {
imageView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
imageView.contentMode = .scaleToFill
blurredEffectView.contentView.addSubview(imageView)
}
public func show(_ view: UIView, inView: Bool = false) {
var window: UIView!
if inView == false, let w = view.window {
window = w
} else {
window = view
}
if blurredEffectView.superview == window {
return
}
let rotation: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.toValue = NSNumber(value: Double.pi * 2)
rotation.duration = 1
rotation.isCumulative = true
rotation.repeatCount = Float.greatestFiniteMagnitude
imageView.layer.add(rotation, forKey: "rotationAnimation")
imageView.center = window.center
blurredEffectView.frame = window.bounds
window.addSubview(blurredEffectView)
blurredEffectView.fadeIn()
}
}
Possible solutions:
1) Make the loading happens in Application Window when you want a full screen Loading (inview == false by default) and keep loadWebview() in viewDidLoad
public func show(_ view: UIView? = nil, inView: Bool = false) {
var window: UIView!
if inView == false, let w = UIApplication.shared.delegate?.window {
window = w
} else {
window = view
}
...
2) Move loadWebview() from viewDidLoad to viewDidAppear
The important part that is moving here is Loading.shared.show(self.view). I wasn't able to animate the components of my view until it has finished laying out (which happens exactly in viewDidAppear()).
More details: viewDidLoad is called after the MyViewController has been initialised and has initialised the main view but before the view is presented to the UI. This means that viewDidLoad allows me to setup the ViewController and the View, before it is shown to the user for interactions.
Although it was a nice and quick solution, in some situation this may not work as expected as viewDidAppear can be called twice, and hence showing the loading view twice which will result in a weird ux.

How to Detect Back Swipe in WkWebView

Is it possible to detect a back swipe in a web view when there has not been any forward navigation? The following code works fine if the user has moved off the first page but I want to know if they back swiped while on that page.
Update - This is the code I'm using after modifying it per comments. I added the navigation back function and the swipe gesture.
class WebViewClient: UIViewController, WKUIDelegate, WKNavigationDelegate, UIGestureRecognizerDelegate
{
var webView: WKWebView!
public func load(pageLink page: String)
{
print("Opening: " + page)
if webView != nil
{
if let myURL = URL(string:page)
{
webUrlRequest = page
let myRequest = URLRequest(url: myURL)
webView.load(myRequest)
}
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
{
print("Opened: " + webUrlRequest)
}
override func loadView()
{
let pref = WKPreferences()
pref.javaScriptEnabled = true
let config = WKWebViewConfiguration()
config.preferences = pref
webView = WKWebView(frame: .zero, configuration: config)
webView.uiDelegate = self
webView.navigationDelegate = self
webView.allowsBackForwardNavigationGestures = true
view = webView
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void)
{
if navigationAction.navigationType == .backForward && webView.backForwardList.backList.count == 0
{
print("Can't go back any more")
}
decisionHandler(.allow)
}
#objc func backNavigationFunction(_ sender: UIScreenEdgePanGestureRecognizer)
{
let dX = sender.translation(in: view).x
if sender.state == .ended
{
let fraction = abs(dX / view.bounds.width)
if fraction >= 0.35
{
print("Back swipe")
//back navigation code here
}
}
}
override func viewDidLoad()
{
super.viewDidLoad()
let swipeGesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(backNavigationFunction(_:)))
swipeGesture.edges = .left
swipeGesture.delegate = self
webView.addGestureRecognizer(swipeGesture)
}
}
I would implement a UIScreenEdgePanGestureRecognizer to handle this. Add the following to your viewDidLoad
let swipeGesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(backNavigationFunction(_:)))
swipeGesture.edges = .left
swipeGesture.delegate = self
view.addGestureRecognizer(swipeGesture)
Then, implement a function to handle the backNavigationFunction like below:
#objc func backNavigationFunction(_ sender: UIScreenEdgePanGestureRecognizer) {
let dX = sender.translation(in: view).x
if sender.state == .ended {
let fraction = abs(dX / view.bounds.width)
if fraction >= 0.35 {
//back navigation code here
}
}
}
Don't forget to make the ViewController inherit from the UIGestureRecognizerDelegate class (i.e. class WebViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, UIGestureRecognizerDelegate). The delegate in question here is UIGestureRecognizerDelegate but I anticipate you'll need the others as well.

Trying to create an iOS WKWebView with a size smaller than the screen programmatically

Im starting with a simple app that just shows a web view using WKWebView.
Everything builds, and works okay, except that it is always fullscreen. Ideally it would not extend under the iOS status bar at the top. It needs to be lowered by 20px to be below it. I have searched extensively for a solution but nothing changes the size. Nothing is setup in InterfaceBuilder, I'm doing everything programmatically. Using Swift.
This seems like something basic that many apps with a WKWebView would do. It should be simple. I'm probably missing something obvious. Any help is appreciated. Thanks in advance.
This is what I have so far:
import UIKit
import WebKit
class ViewController: UIViewController, UIGestureRecognizerDelegate , UIWebViewDelegate, WKNavigationDelegate {
let url = NSURL(string:"http://stackoverflow.com")
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame: CGRect( x: 0, y: 20, width: 380, height: 150 ), configuration: WKWebViewConfiguration() )
self.view = webView
self.view.frame = webView.frame
let req = NSURLRequest(URL:url!)
webView.loadRequest(req)
self.webView.allowsBackForwardNavigationGestures = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Many thanks Emilio!! The adjusted code works perfectly!
import UIKit
import WebKit
class ViewController: UIViewController, UIGestureRecognizerDelegate , UIWebViewDelegate, WKNavigationDelegate {
let url = NSURL(string:"http://stackoverflow.com")
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame: CGRect( x: 0, y: 20, width: self.view.frame.width, height: self.view.frame.height - 20 ), configuration: WKWebViewConfiguration() )
self.view.addSubview(webView)
let req = NSURLRequest(URL:url!)
webView.loadRequest(req)
self.webView.allowsBackForwardNavigationGestures = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Your problem is that UIViewControllers presented in certain ways will manage the frame of their view. If you present it modally, as the root controller, or in a navigation bar, for example, setting the fame manually won't have an effect. (Those are only a few examples).
If you want to manage the frame of the view, you can add the webView as a subview to your view controller's view. Modifying it's frame will work then.
One thing I'd like to point out is that calling self.view.frame = webView.frame after calling self.view = webView is redundant, because they are both the same object, so what you are effectively doing is:
self.view.frame = self.view.frame
I hat make room for some icon's and spend a lot of time fixing this. the solution was placing the adjustment in the viewDidAppear function.
import UIKit
import WebKit
class ViewController: UIViewController {
let webView = WKWebView()
let url_base = "http://www.ztatz.nl"
override func loadView() {
self.view = webView
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" {
print(Float(webView.estimatedProgress))
}
}
override func viewDidLoad() {
super.viewDidLoad()
webView.load( url_base )
}
override func viewDidAppear(_ animated: Bool) {
self.webView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width,
height: self.view.frame.height - 100)
}
}
extension WKWebView {
func load(_ urlString: String) {
if let url = URL(string: urlString) {
let request = URLRequest(url: url)
load(request)
}
}
}
The above solutions Didn't Work for me.
So I tried the following solution and it worked.
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
var url: URL
func makeUIView(context: Context) -> WKWebView {
return WKWebView(frame: CGRect(x: 0,y: 0 , width: UIScreen.main.bounds.size.width, height:200))
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.load(request)
}
}
And Call from your struct.
struct TermsOfService: View {
var body: some View {
VStack {
WebView(url: URL(string: "https://in.search.yahoo.com")!)
}
}
}

Resources