Completion Handler executed before Request is complete - swift

I have an app that uses UIImagePickerController to select an image. That image is then passed into an API function. Once that function is complete I pass the result using a delegate to a modal displayed controller with the results. However, the modal controller is presented before the completion block and my error AlerViewController alerts are never called.
The API is run in the background thread, I have set the completion on the main thread (as it updates the UI - presents the modal controller) but it gets called before the completion is fully executed.
Code below;
API Request
func searchImage(with image: UIImage, to viewController: UIViewController, success: #escaping([ViImageResult]?) -> Void) {
var results = [ViImageResult]()
let params = ViUploadSearchParams(image: image)
ViSearch.sharedInstance.uploadSearch(params: params, successHandler: { (data : ViResponseData?) -> Void in
guard let data = data else { return }
if data.result.isEmpty {
AlertViewController.noResultsFound(viewController: viewController)
return
} else {
if data.hasError {
AlertViewController.dataError(viewController: viewController)
return
} else {
for response in data.result {
results.append(response)
}
DispatchQueue.main.async {
success(results)
}
}
}
}, failureHandler: {
(error) -> Void in
AlertViewController.dataError(viewController: viewController)
})
}
Controller
var selectedImage: UIImage? {
didSet {
guard let selectedImage = selectedImage else { return }
ViSearchSDKService.shared.searchImage(with: selectedImage, to: self) { (results) in
guard let results = results else { return }
if self.resultsDelegate != nil {
self.resultsDelegate?.recievedResults(recievedResults: results)
}
}
let resultsController = ResultsViewController()
self.resultsDelegate = resultsController
let navigationController = UINavigationController(rootViewController: resultsController)
navigationController.modalPresentationStyle = .overFullScreen
DispatchQueue.main.async {
self.present(navigationController, animated: true, completion: nil)
}
}
}
In the API Request, all my AlertViewController functions are called on the main thread and then returns out of the request. Success block is also called on the main thread.
What am I doing wrong here?...
Update
I am not quite sure why this works but it does everything I need. I have moved the API Request into another function outside of
var selectedImage: UIImage? {
didSet {
in my controller.
New Working Code
var selectedImage: UIImage? {
didSet {
guard let selectedImage = selectedImage else { return }
self.searchImage(with: selectedImage)
}
}
func searchImage(with image: UIImage) {
ViSearchSDKService.shared.searchImage(with: image, to: self) { (results) in
guard let results = results else { return }
let resultsController = ResultsViewController()
self.resultsDelegate = resultsController
if self.resultsDelegate != nil {
self.resultsDelegate?.recievedResults(recievedResults: results)
}
let navigationController = UINavigationController(rootViewController: resultsController)
navigationController.modalPresentationStyle = .fullScreen
DispatchQueue.main.async {
self.present(navigationController, animated: true, completion: nil)
}
}
}

I think you want this.
var selectedImage: UIImage? {
didSet {
// make sure it was not set to nil
guard let selectedImage = selectedImage else { return }
// set up your view controller for the response
let resultsController = ResultsViewController()
self.resultsDelegate = resultsController
let navigationController = UINavigationController(rootViewController: resultsController)
// do your search
ViSearchSDKService.shared.searchImage(with: selectedImage, to: self) { (results) in
// leave no path without visible side-effect
guard let results = results else { debugPrint("nil results"); return }
// now that we have the result, present your results view controller
navigationController.modalPresentationStyle = .overFullScreen
DispatchQueue.main.async {
self.present(navigationController, animated: true) { in
// once done presenting, let it know about the results
self.resultsDelegate?.recievedResults(recievedResults: results)
}
}
}
}
}

Related

UIImagePickerController not initiating next function

I'm using Swift 5 and have integrated MessageKit into my app. Here's the expected functionality:
User selects the image via picker
uploadImage function is initiated
Image is uploaded and displayed
What's actually happening:
User selects the image via picker
Nothing happens from there.
I'm not getting a single error message. I've put in print statements to see if it's even entering the uploadImage() function but it's not kicking off. I've change the uipickerimagecontroller code to exact code in other places of my app that is working and even that's not kicking off the function. I know my code isn't pretty but I'm still learning (please don't judge lol). Can anyone help:
Variable setup
private var isSendingPhoto = true {
didSet {
DispatchQueue.main.async {
self.messageInputBar.leftStackViewItems.forEach { item in
item.inputBarAccessoryView?.isUserInteractionEnabled = !self.isSendingPhoto
}
}
}
}
ViewDidLoad
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messageInputBar.delegate = self as? InputBarAccessoryViewDelegate
messagesCollectionView.messagesDisplayDelegate = self
title = "MaybeDrinks"
// 1
let cameraItem = InputBarButtonItem(type: .system)
cameraItem.tintColor = .primary
cameraItem.image = #imageLiteral(resourceName: "camera")
// 2
cameraItem.addTarget(
self,
action: #selector(cameraButtonPressed),
for: .primaryActionTriggered
)
cameraItem.setSize(CGSize(width: 60, height: 30), animated: false)
messageInputBar.leftStackView.alignment = .center
messageInputBar.setLeftStackViewWidthConstant(to: 50, animated: false)
messageInputBar.setStackViewItems([cameraItem], forStack: .left, animated: false) // 3
}
private func sendPhoto(_ image: UIImage) {
isSendingPhoto = true
uploadImage(image) { [weak self] url in
guard let `self` = self else {
return
}
self.isSendingPhoto = false
guard let url = url else {
return
}
var message = Message(messageuser: self.sender, image: image)
message.downloadURL = url
self.save(message)
self.messagesCollectionView.scrollToBottom()
}
}
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
picker.dismiss(animated: true, completion: nil)
// 1
if let asset = info[.phAsset] as? PHAsset {
let size = CGSize(width: 500, height: 500)
PHImageManager.default().requestImage(
for: asset,
targetSize: size,
contentMode: .aspectFit,
options: nil) { result, info in
print("I'm in image picker")
guard let image = result else {
return
}
self.sendPhoto(image)
}
// 2
} else if let image = info[.originalImage] as? UIImage {
sendPhoto(image)
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion: nil)
}
// MARK: - Actions
#objc private func cameraButtonPressed() {
let picker = UIImagePickerController()
picker.delegate = self as? UIImagePickerControllerDelegate & UINavigationControllerDelegate
picker.allowsEditing = true
if UIImagePickerController.isSourceTypeAvailable(.camera) {
picker.sourceType = .camera
} else {
picker.sourceType = .photoLibrary
}
present(picker, animated: true, completion: nil)
}
Upload Image function
private func uploadImage(_ image: UIImage, completion: #escaping (URL?) -> Void) {
print("im in upload")
// STEP 1. Declare URL, Request and Params
let url = URL(string: "https://localhost/messagepost.php")!
// declaring reqeust with further configs
var request = URLRequest(url: url)
// POST - safest method of passing data to the server
request.httpMethod = "POST"
// values to be sent to the server under keys (e.g. ID, TYPE)
let params = ["sender_id": user_id, "uuid": uuid, "sender": me, "recipient_id": rid, "recipient": recipient, "puuid": puuid]
// body
let boundary = "Boundary-\(UUID().uuidString)"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
// Compressing image and converting image to 'Data' type
guard let scaledImage = image.scaledToSafeUploadSize,
let data = scaledImage.jpegData(compressionQuality: 0.4) else {
return
}
// assigning full body to the request to be sent to the server
request.httpBody = createBodyWithParams(params, filePathKey: "file", imageDataKey: data, boundary: boundary)
print(request.httpBody as Any, "\(puuid).jpg")
URLSession.shared.dataTask(with: request) { (data, response, error) in
DispatchQueue.main.async {
// error occured
if error != nil {
Helper().showAlert(title: "Server Error", message: error!.localizedDescription, in: self)
return
}
do {
// save mode of casting any data
guard let data = data else {
Helper().showAlert(title: "Data Error", message: error!.localizedDescription, in: self)
return
}
// fetching JSON generated by the server - php file
let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? NSDictionary
// save method of accessing json constant
guard let parsedJSON = json else {
return
}
// uploaded successfully
if parsedJSON["status"] as! String == "200" {
let newurl = parsedJSON["path"]
print("did you upload", newurl as Any)
print("did you upload", parsedJSON["message"] as Any)
self.isSendingPhoto = true
guard let url = newurl else {
return
}
//uploadImage(image)
var message = Message(messageuser: self.sender, image: image)
message.downloadURL = url as? URL
self.save(message)
self.messagesCollectionView.scrollToBottom()
} else {
// show the error message in AlertView
if parsedJSON["message"] != nil {
let message = parsedJSON["message"] as! String
Helper().showAlert(title: "Error", message: message, in: self)
print("where am i", parsedJSON["message"] as Any)
}
}
} catch {
Helper().showAlert(title: "JSON Error", message: error.localizedDescription, in: self)
print("where am i 2")
}
}
}.resume()
let imageName = [UUID().uuidString, String(Date().timeIntervalSince1970)].joined()
}
private func save(_ message: Message) {
self.messagesCollectionView.scrollToBottom()
}
Make sure your view controller declares conformance to both UIImagePickerControllerDelegate and UINavigationControllerDelegate in either the class declaration:
class DirectMessageViewController: MessagesViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
[...]
}
or in one or more extensions:
extension DirectMessageViewController: UIImagePickerControllerDelegate {
[...]
}
extension DirectMessageViewController: UINavigationControllerDelegate {
[...]
}

Nested completion handlers that do not return items to be sent to TableViewController

I want to get some data from firebaseFirestore and download an image URL from firebaseStorage, while preparing for my segue that will bring the user to the TableViewController, where they will be displayed. Even when using some nested completion handlers (perhaps I made the code too longs), I'm still not able to perform my asyncronous tasks in order, thus rushing too early to the segue. For simplicity I'm using the single segue (no identifiers). In the ViewControllerForTable I have stated a variable var cells : [Cella] = [] globally.
let firestoreUsersReference = Firestore.firestore().collection("users")
let storageReference = Storage.storage()
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationVC = segue.destination as! ViewControllerForTable
prepareDataForSegue(firestoreReference: firestoreUsersReference) { (cella) in
destinationVC.cells = cella
print(destinationVC.cells)
}
}
func getImagesDownloaded(reference: StorageReference, completion: #escaping (UIImage?,Error?)->()) {
reference.getData(maxSize: 10*1024*1024) { (data, error) in
guard error == nil, let data = data else {
completion(nil,error)
return
}
guard let image = UIImage(data: data) else {
completion(nil, FirebaseErrors.expectedImage)
return
}
completion(image,nil)
}
}
enum FirebaseErrors: Error {
case expectedImage
}
func prepareDataForSegue (firestoreReference: CollectionReference, completion : #escaping ([Cella])->()) {
var cellaArray : [Cella] = []
firestoreUsersReference.getDocuments { (querySnapshot, err) in
if err != nil {
print("There has been an error \(String(describing: err?.localizedDescription))")
}
else {
self.getDocumentsFromFirestore(querySnapshot: querySnapshot, completion: { (title, description, image) in
let newCell = Cella(image: image, title: title, bodyMessage: description)
print("NEW CELL : \(newCell)")
cellaArray.append(newCell)
})
}
}
completion(cellaArray)
}
func getDocumentsFromFirestore (querySnapshot: QuerySnapshot?, completion: #escaping (String,String,UIImage)->()) {
for documents in querySnapshot!.documents {
print("\(documents.documentID) => \(documents.data())")
let data = documents.data()
let title = data["userTitle"] as? String
let description = data["userDescription"] as? String
let imageURL = data["userImageURL"] as! String
print("Title: \(String(describing: title)), Description: \(String(describing: description)), imageURL: \(imageURL)")
let storagePath = Storage.storage().reference(forURL: imageURL)
self.getImagesDownloaded(reference: storagePath, completion: { (image, error) in
guard let image = image, error == nil else {
print(String(describing : error?.localizedDescription))
return
}
print("TITLE: \(String(describing: title)), IMAGE: \(image)")
completion(title!, description!, image)
})
}
}
If I understand your question correctly, here what you need to do:
Disconnect the segue for the button in your storyboard.
In the IBAction function for the button, do your prepareDataForSegue work
Once the completion handler is called, call performSegue, which will call the prepareSegue, where you can assign the downloaded cella.
This should load the tableVC only when the data is available.

whose view is not in the windows heirarchy

I know this question has been asked before but none of the answers in the other questions worked for me. Here is my code:
```
var values = [String: AnyObject]()
func loginUserToFirebase(_ completion: () -> Void) {
let accessToken = FBSDKAccessToken.current()
guard let accessTokenString = accessToken?.tokenString else {fatalError()}
let credentials = FIRFacebookAuthProvider.credential(withAccessToken: accessTokenString)
FIRAuth.auth()?.signIn(with: credentials, completion: { (user, error) in
if error != nil {
print(error ?? "Something went wrong")
return
}
self.fbGraphRequest()
})
}
internal func fbGraphRequest(){
FBSDKGraphRequest(graphPath: "/me", parameters: ["fields": "id, name, email"]).start { (connection, result, error) in
if error != nil {
print(error ?? "error unknown")
return
} else {
print(result ?? "no result")
self.values = result as! [String: AnyObject]
print(self.values)
weak var rootViewModel = RootViewModel()
rootViewModel?.values = self.values
self.presentRootViewController()
}
}
}
internal func presentRootViewController() {
let loginController = LoginController()
let rootViewController = RootViewController()
loginController.present(rootViewController, animated: true,
completion: nil)
}
```
and here is my error:
Attempt to present <Art_Cache.RootViewController: 0x7fa6a1c2aab0> on <Art_Cache.LoginController: 0x7fa6a1c840b0> whose view is not in the window hierarchy!
This snippet worked when I had this in my LoginViewController and I used self.present(rootViewController, animation: true, completion: nil). Im trying to convert my project to MVVM and this is what's happening. The problem seems to be around the self.presentRootViewController(). These functions are fired upon pressing the facebook login button. Please help and cheers!
This happens when you current view controller's is not the part of window, could you change
func topViewController(_ base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(nav.visibleViewController)
}
if let tab = base as? UITabBarController {
let moreNavigationController = tab.moreNavigationController
if let top = moreNavigationController.topViewController, top.view.window != nil {
return topViewController(top)
} else if let selected = tab.selectedViewController {
return topViewController(selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(presented)
}
return base
}
topViewController()?.present(rootViewController, animated: true,
completion: nil)

cancel button using MBProgressView

I am trying to use cancel button with the MBProgressView. I am getting the error "cannot convert value of type '()' to expected argument type 'Selector'"
hud.button.addTarget(hud.progressObject, action: cancelButton(), for: .touchUpInside)
I have also tried doing this:
hud.button.addTarget(hud.progressObject, action: #selector(cancelButton), for: .touchUpInside)
and I got the error "Argument of #selector cannot refer to local function 'cancelButton()'".
Can anyone explain to me what am i doing wrong?
cancelButton should be in viewDidLoad or at least I need to find a way to access what's inside viewDidload, because I need to use hud and snapshot.progress to cancel the download:
override func viewDidLoad() {
super.viewDidLoad()
let appdelegate = UIApplication.shared.delegate as! AppDelegate
appdelegate.orintation = UIInterfaceOrientationMask.allButUpsideDown
if book?.bookPath != book?.bookPath {
print("HERE \(book?.bookPath)")
loadReader(filePaht: (book?.bookPath)!)
} else {
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let strName = book?.id
let filePath = "\(documentsPath)/"+strName!+".pdf"
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath) {
loadReader(filePaht: filePath)
return;
}
print("DOWNLOAD #1")
let reference = FIRStorage.storage().reference(forURL: (self.book?.bookURL)!)
let downloadTask = reference.data(withMaxSize: 50 * 1024 * 1024) { (data, error) -> Void in
if (error != nil) {
} else {
if ((try! data?.write(to: URL.init(fileURLWithPath: filePath, isDirectory: false))) != nil) {
self.db.upDate(id: (self.book?.id)!, bookPath: filePath)
self.loadReader(filePaht: filePath)
}
}
}
downloadTask.observe(.resume) { (snapshot) -> Void in
// Download resumed, also fires when the download starts
}
downloadTask.observe(.pause) { (snapshot) -> Void in
// Download paused
}
downloadTask.observe(.progress) { (snapshot) -> Void in
DispatchQueue.global(qos: .default).async(execute: {() -> Void in
self.showHUDWithCancel("Downloading")
DispatchQueue.main.async(execute: {() -> Void in
})
})
self.hud.progressObject = snapshot.progress
}
downloadTask.observe(.success) { (snapshot) -> Void in
// Download completed successfully
print("Download Success")
SwiftLoader.hide()
}
downloadTask.observe(.failure) { (snapshot) -> Void in
//Download failed
print("Download failed")
}
}
}
func showHUDWithCancel(_ aMessage: String) {
self.hud = MBProgressHUD.showAdded(to: self.view, animated: true)
self.hud.mode = MBProgressHUDMode.annularDeterminate
self.hud.label.text = aMessage
self.hud.detailsLabel.text = "Tap to cancel"
let tap = UITapGestureRecognizer(target: self, action: #selector(cancelButton))
self.hud.addGestureRecognizer(tap)
}
func cancelButton() {
self.hud.hide(animated: true)
self.hud.progressObject?.cancel()
print("cancel button is working")
}
This is the Cancel Button function
func cancelButton() {
MBProgressHUD.hide(for: view, animated: true)
snapshot.progress?.pause()
}
Try this -
Call below showHUDWithCancel from where you want to add hud with Cancel.
class ViewController: UIViewController {
var hud = MBProgressHUD()
override func viewDidLoad() {
super.viewDidLoad()
}
func showHUDWithCancel(_ aMessage: String) {
self.hud = MBProgressHUD.showAdded(to: self.view, animated: true)
self.hud.label.text = aMessage
self.hud.detailsLabel.text = "Tap to cancel"
let tap = UITapGestureRecognizer(target: self, action: #selector(cancelButton))
self.hud.addGestureRecognizer(tap)
}
func cancelButton() {
self.hud.hide(animated: true)
// do your other stuff here.
}
}
Add this code within your viewDidLoad it will work.
override func viewDidLoad() {
super.viewDidLoad()
let appdelegate = UIApplication.shared.delegate as! AppDelegate
appdelegate.orintation = UIInterfaceOrientationMask.allButUpsideDown
if book?.bookPath != book?.bookPath {
print("HERE \(book?.bookPath)")
loadReader(filePaht: (book?.bookPath)!)
} else {
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let strName = book?.id
let filePath = "\(documentsPath)/"+strName!+".pdf"
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath) {
loadReader(filePaht: filePath)
return;
}
print("DOWNLOAD #1")
let reference = FIRStorage.storage().reference(forURL: (self.book?.bookURL)!)
downloadTask = reference.data(withMaxSize: 50 * 1024 * 1024) { (data, error) -> Void in
if (error != nil) {
} else {
if ((try! data?.write(to: URL.init(fileURLWithPath: filePath, isDirectory: false))) != nil) {
self.db.upDate(id: (self.book?.id)!, bookPath: filePath)
self.loadReader(filePaht: filePath)
}
}
}
downloadTask.observe(.resume) { (snapshot) -> Void in
// Download resumed, also fires when the download starts
}
downloadTask.observe(.pause) { (snapshot) -> Void in
// Download paused
}
downloadTask.observe(.progress) { (snapshot) -> Void in OperationQueue.main.addOperation {
OperationQueue.main.addOperation {
self.hud.progressObject = snapshot.progress
self.showHUDWithCancel("Downloading")
}
}
}
downloadTask.observe(.success) { (snapshot) -> Void in OperationQueue.main.addOperation {
// Download completed successfully
print("Download Success")
OperationQueue.main.addOperation {
SwiftLoader.hide()
}
}
}
downloadTask.observe(.failure) { (snapshot) -> Void in OperationQueue.main.addOperation {
//Download failed
print("Download failed")
OperationQueue.main.addOperation {
_ = self.navigationController?.popViewController(animated: false)
}
}
}
}
}
Move definition of downloadTask outside of the viewDidLoad method scope into the class itself. This way you'll be able to access task directly, not via snapshot passed in observers, or progress attached to either downloadTask or progressHUD. Doing so you could access task from any method of your view controller including cancelButton():
task.pause()
instead of
snapshot.progress?.pause()
Final code could look like:
class ViewController: UIViewController {
var downloadTask: FIRStorageDownloadTask!
...
override func viewDidLoad() {
super.viewDidLoad()
...
let reference = FIRStorage.storage().reference(forURL: (self.book?.bookURL)!)
downloadTask = reference...
...
}
}
NOTICE: For those of you who use the latest version of MBProgressView, the button documentation has been changed:
/**
* A button that is placed below the labels. Visible only if a target / action is added and a title is assigned..
*/
So, the creation should look something like the following:
class Tools {
static func popLoadingDialog(viewParent: UIView,
label: String,
cancelTarget: Any? = nil,
cancelSelector: Selector? = nil) -> MBProgressHUD {
let loadingNotification = MBProgressHUD.showAdded(to: viewParent, animated: true)
loadingNotification.mode = MBProgressHUDMode.indeterminate
loadingNotification.label.text = label
if(cancelSelector != nil) {
loadingNotification.button.setTitle("Cancel", for: .normal)
loadingNotification.button.addTarget(cancelTarget, action: cancelSelector!, for: .touchUpInside)
}
return loadingNotification
}
}
and call it:
loadingIndicator = Tools.createLoadingDialog(viewParent: view,
label: "Please wait...",
cancelTarget: self,
cancelSelector: #selector(onCancelClick))
loadingIndicator?.show(animated: true)
}
#objc func onCancelClick(){
// do something when the user click on cancel...
}

WKWebView Screenshots

I am trying to capture the image that the webview is displaying to the user, so I can some color analysis of the web page. When I try to get the image from it's parent, I am basically getting a white box, even though the page has rendered:
func makeImageSnapshot()-> (NSImage)
{
let imgSize = self.view.bounds.size
let bir = self.viewbitmapImageRepForCachingDisplayInRect(self.webView!.view.bounds)
bir.size = imgSize
self.webView.cacheDisplayInRect(self.view.bounds, toBitmapImageRep:bir)
let image = NSImage(size:imgSize)
image.addRepresentation(bir)
self.image = image
return image
}
func saveSnapshot()
{
let imgRep = self.image!.representations[0]
let data = imgRep.representationUsingType(NSBitmapImageFileType.NSPNGFileType, properties: nil)
data.writeToFile("/tmp/file.png", atomically: false)
}
It looks to me like I can't get access to the properties of the actual view (in this case the bounds) inside of the webView. When I try to access it, the compiler barfs:
/Users/josh/Canary/MacOsCanary/canary/canary/Modules/Overview/Overview.swift:55:37: '(NSView!, stringForToolTip: NSToolTipTag, point: NSPoint, userData: UnsafePointer<()>) -> String!' does not have a member named 'bounds'
My guess is that this is happening due to the extensions approach used by OS X and iOS. Any ideas, or should I just go back to using the legacy WebView?
I realise the question was for Mac OS X, but I found this page whilst searching for an iOS solution. My answer below doesn't work on Mac OS X as the drawViewHierarchyInRect() API call is currently iOS only, but I put it here for reference for other iOS searchers.
This Stackoverflow answer solved it for me on iOS 8 with a WKWebView. That answer's sample code is in Objective-C but the Swift equivalent to go in a UIView sub-class or extension would be along the lines of the code below. The code ignores the return value of drawViewHierarchyInRect(), but you may want to pay attention to it.
func imageSnapshot() -> UIImage
{
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0);
self.drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true);
let snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return snapshotImage;
}
Swift 3
extension WKWebView {
func screenshot() -> UIImage? {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0);
self.drawHierarchy(in: self.bounds, afterScreenUpdates: true);
let snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return snapshotImage;
}
}
Note: This solution only works on iOS.
Found myself in the same boat today but found a solution (by using private APIs).
If you're not targeting the App Store and generally are not afraid of using private APIs, here's a way to capture screenshots of WKWebView's on OS X:
https://github.com/lemonmojo/WKWebView-Screenshot
You will need to have access to a target writeable place - the snapshotURL ie.., such as the desktop, so we provide a handler for that:
func registerSnaphotsURL(_ sender: NSMenuItem, handler: #escaping (URL) -> Void) {
var targetURL : URL
// 1st around authenticate and cache sandbox data if needed
if isSandboxed, desktopData == nil {
targetURL =
UserSettings.SnapshotsURL.value.count == 0
? getDesktopDirectory()
: URL.init(fileURLWithPath: UserSettings.SnapshotsURL.value, isDirectory: true)
let openPanel = NSOpenPanel()
openPanel.message = "Authorize access to "
openPanel.prompt = "Authorize"
openPanel.canChooseFiles = false
openPanel.canChooseDirectories = true
openPanel.canCreateDirectories = true
openPanel.directoryURL = targetURL
openPanel.begin() { (result) -> Void in
if (result == .OK) {
targetURL = openPanel.url!
// Since we do not have data, clear any bookmark
if self.storeBookmark(url: targetURL, options: self.rwOptions) {
self.desktopData = self.bookmarks[targetURL]
UserSettings.SnapshotsURL.value = targetURL.absoluteString
if !self.saveBookmarks() {
print("Yoink, unable to save snapshot bookmark")
}
self.desktopData = self.bookmarks[targetURL]
handler(targetURL)
}
}
else
{
return
}
}
}
else
{
targetURL =
UserSettings.SnapshotsURL.value.count == 0
? getDesktopDirectory()
: URL.init(fileURLWithPath: UserSettings.SnapshotsURL.value, isDirectory: true)
handler(targetURL)
}
}
we wanted to allow single (view controller) and all current views (app delegate) so two actions in their respective files, both making use of the register handler.
App Delegate
#objc #IBAction func snapshotAllPress(_ sender: NSMenuItem) {
registerSnaphotsURL(sender) { (snapshotURL) in
// If we have a return object just call them, else notify all
if let wvc : WebViewController = sender.representedObject as? WebViewController {
sender.representedObject = snapshotURL
wvc.snapshot(sender)
}
else
{
sender.representedObject = snapshotURL
let notif = Notification(name: Notification.Name(rawValue: "SnapshotAll"), object: sender)
NotificationCenter.default.post(notif)
}
}
}
View Controller
func viewDidLoad() {
NotificationCenter.default.addObserver(
self,
selector: #selector(WebViewController.snapshotAll(_:)),
name: NSNotification.Name(rawValue: "SnapshotAll"),
object: nil)
}
#objc func snapshotAll(_ note: Notification) {
snapshot(note.object as! NSMenuItem)
}
view singleton action
#objc #IBAction func snapshotPress(_ sender: NSMenuItem) {
guard let url = webView.url, url != webView.homeURL else { return }
guard let snapshotURL = sender.representedObject as? URL else {
// Dispatch to app delegate to handle a singleton
sender.representedObject = self
appDelegate.snapshotAllPress(sender)
return
}
sender.representedObject = snapshotURL
snapshot(sender)
}
the webView interaction to capture an image
#objc func snapshot(_ sender: NSMenuItem) {
guard let url = webView.url, url != webView.homeURL else { return }
guard var snapshotURL = sender.representedObject as? URL else { return }
// URL has only destination, so add name and extension
let filename = String(format: "%# Shapshot at %#",
(url.lastPathComponent as NSString).deletingPathExtension,
String.prettyStamp())
snapshotURL.appendPathComponent(filename)
snapshotURL = snapshotURL.appendingPathExtension("png")
webView.takeSnapshot(with: nil) { image, error in
if let image = image {
self.webImageView.image = image
DispatchQueue.main.async {
self.processSnapshotImage(image, to: snapshotURL)
}
}
else
{
self.userAlertMessage("Failed taking snapshot", info: error?.localizedDescription)
self.webImageView.image = nil
}
}
}
and the capture to the targeted area
func processSnapshotImage(_ image: NSImage, to snapshotURL: URL) {
guard let tiffData = image.tiffRepresentation else { NSSound(named: "Sosumi")?.play(); return }
let bitmapImageRep = NSBitmapImageRep(data: tiffData)
do
{
try bitmapImageRep?.representation(using: .png, properties: [:])?.write(to: snapshotURL)
// https://developer.apple.com/library/archive/qa/qa1913/_index.html
if let asset = NSDataAsset(name:"Grab") {
do {
// Use NSDataAsset's data property to access the audio file stored in Sound.
let player = try AVAudioPlayer(data:asset.data, fileTypeHint:"caf")
// Play the above sound file.
player.play()
} catch {
print("no sound for you")
}
}
if snapshotURL.hideFileExtensionInPath(), let name = snapshotURL.lastPathComponent.removingPercentEncoding {
print("snapshot => \(name)")
}
} catch let error {
appDelegate.userAlertMessage("Snapshot failed", info: error.localizedDescription)
}
}