How to obtain URL value of KVO #keyPath(WKWebView.url)? - ios

I've posted a more in-depth question to try and get to the bottom of the issue, but in a brief:
I'm attempting to show a PHP/JS-based web application (Laravel) through a WKWebView. However, due to the nature of the script's redirecting properties, the only code I've gotten to actually detect the URL change is with #keyPath(WKWebView.url):
override func viewDidLoad() {
super.viewDidLoad()
webView.navigationDelegate = self
webView.uiDelegate = self
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(WKWebView.url) {
print("URL Change:", self.webView.url?.absoluteString ?? "# No value provided")
}
}
However, the output to console is always the same:
URL Change: # No value provided
So I know that the KVO for WKWebView.url is able to fire upon script-based redirection within the WebView. In fact, if you take a look at my other question, it is the only code that can detect this sort of redirection – which is strange, because when launched in Safari (both iOS and macOS), the URL bar is able to reflect those redirected changes to the URL's value. However, when in the WKWebView, none of the WKNavigationDelegate functions are able to detect such a change to the URL.
Is there any way to obtain the URL directly from the keyPath value of WKWebView.url when fired? Are there any alternatives, not described in my previously-mentioned question, that could obtain the URL?
Trying to obtain the URL value from webView.url seems to always return nil.
EDIT: I am able to get the exact URL value with the observerValue function code:
if let key = change?[NSKeyValueChangeKey.newKey] {
print("URL: \(key)") // url value
}
However, I am unable to cast it as a String or pass it to another function otherwise. Is there any way to set this key as a variable, if it .contains("https://")?

I was able to assign the KVO WKWebView.url to a String variable. From there, I was able to pass the String value to a function that then handles each output I'm looking for:
var cURL = ""
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let key = change?[NSKeyValueChangeKey.newKey] {
cURL = "\(key)" // Assign key-value to String
print("cURL:", cURL) // Print key-value
cURLChange(url: cURL) // Pass key-value to function
}
}
func cURLChange(url: String) {
if cURL.contains("/projects/new") {
print("User launched new project view")
// Do something
} else {
// Do something else
}
}
A similar solution, using a more modern method (with less hassle), was provided here.
var cURL = ""
var webView: WKWebView!
var webViewURLObserver: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
// 1. Assign changed value to variable
webViewURLObserver = webView.observe(\.url, options: .new) { webView, change in
self.cURL = "\(String(describing: change.newValue))" }
// 2. Print value of WKWebView URL
webViewURLObserver = webView.observe(\.url, options: .new) { webView, change in
print("URL: \(String(describing: change.newValue))"
)}
By using NSKeyValueObservation of an object, you don't need to remove observers or check observer values by keyPath. You can simply set it to observe an object (ie. WKWebView) and run code when a change is observed.

Related

How to get WhiteBalance (Kelvin) from captureOutput

Apple has various ways to change and view the Kelvin of using AVCaptureDevice
https://developer.apple.com/documentation/avfoundation/avcapturedevice/white_balance
Example:
guard let videoDevice = AVCaptureDevice
.default(.builtInWideAngleCamera, for: .video, position: .back) else {
return
}
guard
let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice),
captureSession.canAddInput(videoDeviceInput) else {
print("There seems to be a problem with the camera on your device.")
return
}
captureSession.addInput(videoDeviceInput)
let kelvin = videoDevice.temperatureAndTintValues(for: videoDevice.deviceWhiteBalanceGains)
print("Kelvin temp \(kelvin.temperature)")
print("Kelvin tint \(kelvin.tint)")
let captureOutput = AVCaptureVideoDataOutput()
captureOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)]
captureOutput.setSampleBufferDelegate(self, queue: DispatchQueue.global(qos: DispatchQoS.QoSClass.default))
captureSession.addOutput(captureOutput)
This will always return
Kelvin temp 3900.0889
Kelvin tint 4.966322
How can I get the White Balance (Kelvin value) through the live camera feed?
It is giving you one value because videoDevice.temperatureAndTintValues(for: videoDevice.deviceWhiteBalanceGains) is called only once. To get an updating value following the camera feed, you have two options:
Key-value observing, which will notify when the WB changes
Call the function videoDevice.temperatureAndTintValues(for: videoDevice.deviceWhiteBalanceGains) for each frame.
I would suggest you use the second, key-value observing is somewhat annoying. In that case, I guess you already have implemented the method func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) of the AVCaptureVideoDataOutputSampleBufferDelegate. That method is called every time a frame is returned, so, you can update your videoDevice.temperatureAndTintValues for each frame of the live camera feed.
For the key-value observing, you first setup the observer (e.g in viewDidAppear), for example:
func addObserver() {
self.addObserver(self, forKeyPath: "videoDevice.deviceWhiteBalanceGains", options: .new, context: &DeviceWhiteBalanceGainsContext)
}
Keep a reference to the videoDevice, declaring it this way:
#objc dynamic var videoDevice : AVCaptureDevice!
Then #objc and dynamic are needed for the key-value observing.
Now you can implement this function, which will be called every time the observed value changes:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let context = context else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: nil)
return
}
if context == &DeviceWhiteBalanceGainsContext {
// do your work on WB here
}
}
Finally, you can define the context this way (I have it outside my ViewController):
private var DeviceWhiteBalanceGainsContext = 0
I have implemented both methods in my apps and they both work well.
WARNING: sometimes, the WB values will be outside the allowed range (especially at startup) and the API raises an exception. Make sure to handle this otherwise, the app will crash.

WKWebView decidePolicyFor gets called after page loaded

I try to catch the url about to load in WKWebView before it loads. Based on documents decidePolicyFor navigationAction (WKNavigationDelegate) should do the job but my problem is this delegate gets called after new url get loaded not before that.
here is the extension I wrote.
extension MyWebViewController: WKNavigationDelegate {
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
guard let navigationURL = navigationAction.request.url else {
decisionHandler(.allow)
return
}
let forbiddenUrlPattern = Configuration.current.links.forbiddenUrlPattern
if forbiddenUrlPattern.matches(url: navigationURL) {
decisionHandler(.cancel)
showFullScreenError(error: .forbidden)
return
}
// Default policy is to allow navigation to any links the subclass doesn't know about
decisionHandler(.allow)
}
}
*PS the matches extension checks the pattern and it works fine.
now the problem is that the content of forbiddenUrl showed for a time before this delegate func gets called and then the error page comes to screen, and if I close it the last visible webPage is from forbidden link pattern.
is there any way to understand about the link before actually loading it in webView?
I am using Xcode 11.2.1 & Swift 5.0
I wrote the answer I found here that it may help someone else as well.
After lots of struggle I found out decisionHandler wont get called if the url is relative (not absolute urls).
So why decisionHandler got called after loading that page?
the answer I found is: when we have urls like href:"/foo/ba" then after calling that url, it will load and resolve as www.domain.com/foo/ba and only then desicionHandler got called.
Also didCommit only called once when I wanted to load the url for the first time in webView.
so the solution helped me was to add an observer to webView
webView.addObserver(self, forKeyPath: "URL", options: [.new,.old], context: nil)
and
override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
/// This observer is in addition to the navigationAction delegate to block relative urls and intrrupt them and do native
/// action if possible.
if let newValue = change?[.newKey] as? URL, let oldValue = change?[.oldKey] as? URL, newValue != oldValue {
let forbiddenUrlPattern = Configuration.current.links.forbiddenUrlPattern
if forbiddenUrlPattern.matches(url: newValue) {
showFullScreenError(error: .forbidden)
return
}
/// These two action needed to cancel the webView loading the offerDetail page.
/// otherwise as we stop loading the about to start process webView will show blank page.
webView.stopLoading()
///This is small extension for make webView go one step back
webView.handleBackAction(sender: newValue)
return
}
}
}
So this observer in addition to decisionHandler would cover both absolute and relative urls any one wants to listen and take action if needed.

UserDefaults.didChangeNotification not firing

The project I am working on has an extension that writes data to UserDefaults. Then in the containing app should the UI should get updated according to the changes. The problem is that UserDefaults.didChangeNotification does not get fired unless the screen comes from background. What could be the reason and is there a way to be fixed or another way to get the needed update?
Writing the data in the extension:
let sharedUserDefaults = UserDefaults(suiteName: Common.UserDefaultsSuite)
var receivedNotifications = sharedUserDefaults?.array(forKey: Common.ReceivedNotifications)
if receivedNotifications != nil {
receivedNotifications?.append(aData)
} else {
receivedNotifications = [aData]
}
sharedUserDefaults?.set(receivedNotifications, forKey: Common.ReceivedNotifications)
Registering for the notification in the view controller:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange), name: UserDefaults.didChangeNotification, object: nil)
}
And working with changed user defaults (that actually does not get called):
#objc func userDefaultsDidChange(_ notification: Notification) {
print("User defaults did change")
gatherReceivedNotifications()
}
Still no idea why the other way doesn't work but the following works so it's a solution. As per suggested here I did the following:
override func viewDidLoad() {
super.viewDidLoad()
UserDefaults(suiteName: Common.UserDefaultsSuite)?.addObserver(self, forKeyPath: Common.ReceivedNotifications, options: .new, context: nil)
}
Then implemented observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?):
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == Common.ReceivedNotifications {
gatherReceivedNotifications()
}
}
It is fired immediately and only when a change to UserDefaults for the key Common.ReceivedNotifications is made.
The code #selector(userDefaultsDidChange) is means func userDefaultsDidChange() without parameter.
But you defined func userDefaultsDidChange(_ notification: Notification), it's have one parameter.
Next step:
Change #selector(userDefaultsDidChange) to #selector(userDefaultsDidChange(_:)) can fixed it.

How to get notified when Flash is ON or OFF in Auto flash mode due to low light

Am using UIImagePickerController to present the camera and initially make the flash mode to Auto.
videoCapturer.sourceType = UIImagePickerControllerSourceType.Camera
videoCapturer.mediaTypes = [kUTTypeMovie as String]
videoCapturer.cameraFlashMode = UIImagePickerControllerCameraFlashMode.Auto
[self .presentViewController(videoCapturer, animated: true, completion: nil)]
i want to get notified when the flash is set to ON or off according to the lighting.
Just use KVO.
let capture = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
capture.addObserver(self, forKeyPath: "torchActive", options: NSKeyValueObservingOptions.New.union(.Initial), context: nil)
And implement this method:
public override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "torchActive" {
// do something when torchActive changed
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
Here is Apple's description of torchActive:
#property torchActive
#abstract
Indicates whether the receiver's torch is currently active.
#discussion
The value of this property is a BOOL indicating whether the receiver's torch is
currently active. If the current torchMode is AVCaptureTorchModeAuto and isTorchActive
is YES, the torch will illuminate once a recording starts (see AVCaptureOutput.h
-startRecordingToOutputFileURL:recordingDelegate:). This property is key-value observable.

Detect when camera is auto-focusing

I am working on video application. I want to discard the video frames when camera is autofocusing. During autofocus image captured become blurred and image processing for that frame become bad but once autofocus is done, image processing become excellent. Any body give me solution?
adjustingFocus property.
Indicates whether the device is currently adjusting its focus setting. (read-only)
*Notes: You can observe changes to the value of this property using Key-value observing.
iOS 4.0 and later
https://developer.apple.com/library/ios/documentation/AVFoundation/Reference/AVCaptureDevice_Class/Reference/Reference.html#//apple_ref/occ/instp/AVCaptureDevice/adjustingFocus
Following is a sample code in Swift 3.x.
First a observer should be added to the selected capture device at camera initialization.
captureDevice.addObserver(self, forKeyPath: "adjustingFocus", options: [.new], context: nil)
Then observeValue method is overridden. By accessing the optional value returned by the method, autoFocussing frames can be identified.
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let key = keyPath, let changes = change else {
return
}
if key == "adjustingFocus" {
let changedValue = changes[.newKey]
if (changedValue! as! Bool){
// camera is auto-focussing
}else{
// camera is not auto-focussing
}
}
}
Example on Swift 4+
class ViewController: UIViewController, AVCapturePhotoCaptureDelegate {
//#objc var captureDevice: AVCaptureDevice?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.addObservers()
}
func addObservers() {
self.addObserver(self, forKeyPath: "captureDevice.adjustingFocus", options: .new, context: nil)
}
func removeObservers() {
self.removeObserver(self, forKeyPath: "captureDevice.adjustingFocus")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "captureDevice.adjustingFocus" {
print("============== adjustingFocus: \(self.captureDevice?.lensPosition)")
}
} //End of class
Observing adjustingFocus is not working for me. It's always no. And I find this.
Note that when traditional contrast detect auto-focus is in use, the AVCaptureDevice adjustingFocus property flips to YES when a focus is underway, and flips back to NO when it is done. When phase detect autofocus is in use, the adjustingFocus property does not flip to YES, as the phase detect method tends to focus more frequently, but in small, sometimes imperceptible amounts. You can observe the AVCaptureDevice lensPosition property to see lens movements that are driven by phase detect AF.
from Apple
I have not try it yet, I will try and update later.
Edit. I have try it, and confirm this's right.

Resources