SwiftUI runJavaScriptConfirmPanelWithMessage does not work with WKWebView - swift

I'm building a simple webapp with SwiftUI that simply load my website into the webview. From my website, it has to show the Javascript confirm for users to able to logout from the app and my function inside of runJavaScriptConfirmPanelWithMessage crashes the app when Javascript confirm is triggered.
Here is the entire code of my webapp.
import SwiftUI
import WebKit
struct Webview : UIViewRepresentable {
let request: URLRequest
var webview: WKWebView?
init(web: WKWebView?, req: URLRequest) {
self.webview = WKWebView()
self.request = req
}
class Coordinator: NSObject, WKUIDelegate {
var parent: Webview
init(_ parent: Webview) {
self.parent = parent
}
// Delegate methods go here
func webView(_ webView: WKWebView,
runJavaScriptAlertPanelWithMessage message: String,
initiatedByFrame frame: WKFrameInfo,
completionHandler: #escaping () -> Void) {
// alert functionality goes here
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
alertController.present(alertController, animated: true)
completionHandler()
}
func webView(_ webView: WKWebView,
runJavaScriptConfirmPanelWithMessage message: String,
initiatedByFrame frame: WKFrameInfo,
completionHandler: #escaping (Bool) -> Void) {
// confirm functionality goes here. THIS CRASHES THE APP
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action) in
completionHandler(true)
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { (action) in
completionHandler(false)
}))
alertController.present(alertController, animated: true, completion: nil)
}
func webView(_ webView: WKWebView,
runJavaScriptTextInputPanelWithPrompt prompt: String,
defaultText: String?,
initiatedByFrame frame: WKFrameInfo,
completionHandler: #escaping (String?) -> Void) {
// prompt functionality goes here
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
return webview!
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.uiDelegate = context.coordinator
uiView.load(request)
}
func goBack(){
webview?.goBack()
}
func goForward(){
webview?.goForward()
}
func reload(){
webview?.reload()
}
}
struct ContentView: View {
let webview = Webview(web: nil, req: URLRequest(url: URL(string: "https://google.com")!))
var body: some View {
VStack {
webview
HStack() {
Button(action: {
self.webview.goBack()
}){
Image(systemName: "chevron.left")
}.padding(32)
Button(action: {
self.webview.reload()
}){
Image(systemName: "arrow.clockwise")
}.padding(32)
Button(action: {
self.webview.goForward()
}){
Image(systemName: "chevron.right")
}.padding(32)
}.frame(height: 40)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm not sure what I did wrong in this section for runJavaScriptConfirmPanelWithMessage that crashes the app.
Here is the part that I'm having a trouble with.
func webView(_ webView: WKWebView,
runJavaScriptConfirmPanelWithMessage message: String,
initiatedByFrame frame: WKFrameInfo,
completionHandler: #escaping (Bool) -> Void) {
// confirm functionality goes here. THIS CRASHES THE APP
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action) in
completionHandler(true)
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { (action) in
completionHandler(false)
}))
alertController.present(alertController, animated: true, completion: nil)
}
Can anyone tell me what I'm doing wrong here?

Please note that you're trying to present alertController from alertController and that's a crash reason.

You need a View Controller to present alert. Try this:
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: #escaping (Bool) -> Void) {
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet)
alertController.addAction(
UIAlertAction(title: "OK", style: .default, handler: { (action) in completionHandler(true) })
)
alertController.addAction(
UIAlertAction(title: "Cancel", style: .default, handler: { (action) in completionHandler(false) })
)
if let controller = topMostViewController() {
controller.present(alertController, animated: true, completion: nil)
}
}
private func topMostViewController() -> UIViewController? {
guard let rootController = keyWindow()?.rootViewController else {
return nil
}
return topMostViewController(for: rootController)
}
private func keyWindow() -> UIWindow? {
return UIApplication.shared.connectedScenes
.filter {$0.activationState == .foregroundActive}
.compactMap {$0 as? UIWindowScene}
.first?.windows.filter {$0.isKeyWindow}.first
}
private func topMostViewController(for controller: UIViewController) -> UIViewController {
if let presentedController = controller.presentedViewController {
return topMostViewController(for: presentedController)
} else if let navigationController = controller as? UINavigationController {
guard let topController = navigationController.topViewController else {
return navigationController
}
return topMostViewController(for: topController)
} else if let tabController = controller as? UITabBarController {
guard let topController = tabController.selectedViewController else {
return tabController
}
return topMostViewController(for: topController)
}
return controller
}
Thanks to this answer https://stackoverflow.com/a/57877120/11212894

Related

Swift WKWebView webViewDidClose(window.close()) not working

I have a WKWebView
after moving several pages, web do confirm action.
like this:
var Common = {
Close : function (){
if(confirm("confirm?")) window.close();
}
}
in my code:
webView.uiDelegate = self
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
// this method is not invoked
}
func webViewDidClose(_ webView: WKWebView) {
dismiss(animated: true, completion: nil)
}
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: #escaping (Bool) -> Void) {
// this method invoked
let alertController = UIAlertController(title: message, message: nil, preferredStyle: .alert);
let cancelAction = UIAlertAction(title: "cancel", style: .cancel) {
_ in completionHandler(false)
}
let okAction = UIAlertAction(title: "ok", style: .default) {
_ in completionHandler(true)
}
alertController.addAction(cancelAction)
alertController.addAction(okAction)
DispatchQueue.main.async {
self.present(alertController, animated: true, completion: nil)
}
}
in my alertController I called completionHandler(true). but webViewDidClose is not invoked. so I can not dismiss web vc.
what is the problem?
What can I try?
// webView.load(URLRequest)
webView.evaluateJavaScript("window.open(\"\(url.absoluteString)\");", completionHandler: { result, error in
})
override func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
if let currentWebView = createWebView {
currentWebView.load(navigationAction.request)
return nil
}
createWebView = WKWebView(frame: view.bounds, configuration: configuration)
createWebView!.navigationDelegate = self
createWebView!.uiDelegate = self
view.addSubview(createWebView!)
return createWebView!
}

I wanna control crop area in ImagePicker

After selecting the image from the gallery, I can crop it as a still frame to crop it. I don't want this. I want to be able to change the crop size. I want to be able to change the crop area like in the second gif. How can I do this without resorting to a 3rd party library ?
Image Picker Manager:
class ImagePickerManager: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var picker = UIImagePickerController();
var alert = UIAlertController(title: "Fotoğraf Seç", message: nil, preferredStyle: .actionSheet)
var viewController: UIViewController?
var pickImageCallback : ((UIImage) -> ())?;
override init(){
super.init()
let cameraAction = UIAlertAction(title: "Kamerayı Aç", style: .default){
UIAlertAction in
self.openCamera()
}
let galleryAction = UIAlertAction(title: "Galeriden Seç", style: .default){
UIAlertAction in
self.openGallery()
}
let cancelAction = UIAlertAction(title: "İptal", style: .cancel){
UIAlertAction in
}
// Add the actions
picker.allowsEditing = true
picker.delegate = self
alert.addAction(cameraAction)
alert.addAction(galleryAction)
alert.addAction(cancelAction)
}
func pickImage(_ viewController: UIViewController, _ callback: #escaping ((UIImage) -> ())) {
pickImageCallback = callback;
self.viewController = viewController;
alert.popoverPresentationController?.sourceView = self.viewController!.view
viewController.present(alert, animated: true, completion: nil)
}
func openCamera(){
alert.dismiss(animated: true, completion: nil)
if(UIImagePickerController .isSourceTypeAvailable(.camera)){
picker.sourceType = .camera
self.viewController!.present(picker, animated: true, completion: nil)
} else {
let alertController: UIAlertController = {
let controller = UIAlertController(title: "Warning", message: "You don't have camera", preferredStyle: .alert)
let action = UIAlertAction(title: "OK", style: .default)
controller.addAction(action)
return controller
}()
viewController?.present(alertController, animated: true)
}
}
func openGallery(){
alert.dismiss(animated: true, completion: nil)
picker.sourceType = .photoLibrary
self.viewController!.present(picker, animated: true, completion: nil)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[.editedImage] as? UIImage {
pickImageCallback?(image)
} else if let image = info[.originalImage] as? UIImage {
pickImageCallback?(image)
}
picker.dismiss(animated: true, completion: nil)
}
#objc func imagePickerController(_ picker: UIImagePickerController, pickedImage: UIImage?) {
}
}
My code result:
I want do this:

How do I observe user editing a textField in an UIAlertController?

I have an UIAlertController with a textField. Since I can't create an outlet for the textField I have to find another way to get ahold of a function that observes user input in realtime.
I have made some attempts, but not successfully. This thread explain how to add a target to the textField:
How do I check when a UITextField changes?
textfield.addTarget(self, action: #selector(ViewControllerr.textFieldDidChange(_:)), for: UIControl.Event.editingChanged)
I tried this, but don't quite understand the #selector input. Does it want my UIViewController? or my UIAlertController I tried both but got the same error message:
Value of type 'UIViewController' has no member 'textFieldDidChange'
Same with UIAlertController.
I also tried setting my textField to delegate to access functions of that class:
textField.delegate = self as? UITextFieldDelegate
But I guess this isn't the correct approach as I still couldn't access any functions except by doing:
textField.delegate?.textFieldDidBeginEditing?(textField)
But that didn't give me anything either.
I also tried adding an extension like this:
extension UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
print("🦋EDITING")
}// became first responder
func textFieldDidEndEditing(_ textField: UITextField) {
print("🦋EDITING")
}
}
But for some reason I can't access them through textView
If I extend the class like this:
extension PickNumberViewController: UITextFieldDelegate {...}
I get the same error message
Value of type 'UITextField' has no member 'textFieldDel...'
which makes no sense since I set textField to UITextFieldDelegate not UITextField.
Let me know if you would like to see my code as well.
You can try to get a reference to it with self.textF = alert.textFields?[0]
class ViewController: UIViewController {
var textF:UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
//1. Create the alert controller.
let alert = UIAlertController(title: "Some Title", message: "Enter a text", preferredStyle: .alert)
alert.addTextField { (textField) in
textField.text = "Some default text"
}
let tex = alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak alert] (_) in
}))
// 4. Present the alert.
self.present(alert, animated: true, completion: nil)
self.textF = alert.textFields?[0]
self.textF.addTarget(self, action: #selector(self.textFieldDidChange), for: UIControl.Event.editingChanged)
}
}
#objc func textFieldDidChange() {
print(textF.text)
}
}
You don't need to hold a reference to the text field. You can add target or set delegate within the addTextField closure like this.
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let alert = UIAlertController(title: "Title", message: "Message", preferredStyle: .alert)
alert.addTextField { textField in
textField.delegate = self
textField.addTarget(self, action: #selector(self.textChanged(_:)), for: .editingChanged)
textField.placeholder = "Enter name"
}
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
#objc func textChanged(_ sender: UITextField) {
print("Text changed - ", sender.text!)
}
}
extension ViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
print("EDITING")
}
func textFieldDidEndEditing(_ textField: UITextField) {
print("EDITING")
}
}
iOS13+ One option is to use UIAction
extension UIAlertController {
public func addTextField(_ configuration: #escaping ((UITextField) -> Void),
onEditingDidBegin: ((UITextField) -> Void)? = nil,
onEditingChanged: ((UITextField) -> Void)? = nil,
onEditingDidEnd: ((UITextField) -> Void)? = nil) {
self.addTextField { textField in
configuration(textField)
if let onEditingDidBegin {
textField.addAction(UIAction { action in
onEditingDidBegin(action.sender as! UITextField)
}, for: .editingDidBegin)
}
if let onEditingChanged {
textField.addAction( UIAction { action in
onEditingChanged(action.sender as! UITextField)
}, for: .editingChanged)
}
if let onEditingDidEnd {
textField.addAction( UIAction { action in
onEditingDidEnd(action.sender as! UITextField)
}, for: .editingDidEnd)
}
}
}
Use like so:
let alert = UIAlertController(title: "Enter Text", message: nil, preferredStyle: .alert)
alert.addTextField { textField in
textField.text = "Some text"
textField.placeholder = "Placeholder"
} onEditingDidBegin: { textField in
textField.selectAll(nil) // selects all text
} onEditingChanged: { textField in
if let text = textField.text {
textField.textColor = text.count > 5 ? .red : .label
}
} onEditingDidEnd: { textField in
if let text = textField.text {
if text.count > 5 {
textField.text = "This is too long"
}
}
}
You shouldn't use onEditingDidEnd for your final validation though. Put that in the UIAlertAction for your "OK" button and pull out the textFields:
let okAction = UIAlertAction(title: "OK", style: .destructive) { [weak self, weak alert] _ in
if let self,
let alert,
let textField = alert.textFields?.first {
// validate (here, 'self', is a view controller)
self.label.text = textField.text
}
}
alert.addAction(okAction)

Save Login Swift

How can you do so when closing the application do not ask me again to enter email and password? I read about userdefaults and keychainwrapper, but I can not apply any. I do test after test and they ask me again to enter the credentials.
This is the login code:
class LogInViewController: UIViewController, UITextFieldDelegate
{
#IBOutlet weak var emailField: UITextField!
#IBOutlet weak var passwordField: UITextField!
#IBAction func Cancel(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
}
#IBAction func LogIn(_ sender: Any)
{
if self.emailField.text == "" ||
self.passwordField.text == ""
{
let alertController = UIAlertController(title: "Error", message: "Por favor introduce email y contraseña", preferredStyle: .alert)
let defaultAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
alertController.addAction(defaultAction)
self.present(alertController, animated: true, completion: nil)
} else
{
Auth.auth().signIn(withEmail: self.emailField.text!, password: self.passwordField.text!) { (user, error) in
if error == nil
{
let vc = self.storyboard?.instantiateViewController(withIdentifier: "HandleViewSegue")
self.present(vc!, animated: true, completion: nil)
} else
{
let alertController = UIAlertController(title: "Error", message: error?.localizedDescription, preferredStyle: .alert)
let defaultAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
alertController.addAction(defaultAction)
self.present(alertController, animated: true, completion: nil)
}
}
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool
{
emailField.resignFirstResponder()
passwordField.resignFirstResponder()
return (true)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
self.view.endEditing(true)
self.emailField.delegate = self
self.passwordField.delegate = self
}
}

Adding Photos to Array and Displaying to UICollection View error

QUESTION
This code works to set an image to an array but comes up with the following error - 2017-12-06 12:31:21.264812+0000 SmartReceipts[880:172369] [discovery] errors encountered while discovering extensions: Error Domain=PlugInKit Code=13 "query cancelled" UserInfo={NSLocalizedDescription=query cancelled}
I need to know what this means and if there is a possible fix around it?
import UIKit
class GalleryController: UICollectionViewController,
UIImagePickerControllerDelegate,
UINavigationControllerDelegate {
var Receipts = [UIImage?]()
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView?.reloadData()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Register cell classes
//self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
self.dismiss(animated: true, completion: nil)
print(info);
let newReceipts = info[UIImagePickerControllerEditedImage] as? UIImage
self.Receipts.append(newReceipts)
print("Array Contains \(self.Receipts.count) Receipts")
print(self.Receipts)
self.collectionView?.reloadData()
print("completedIfStatement")
}
#IBAction func getReceipts(_ sender: Any) {
print("PlusButtonPressed")
let optionMenu = UIAlertController(title: nil, message: "Choose Option", preferredStyle: .actionSheet)
// 2
let albumAction = UIAlertAction(title: "Album", style: .default, handler: {
(alert: UIAlertAction!) -> Void in
print("PhotosOption")
self.getFromReceipts()
})
let cameraAction = UIAlertAction(title: "Camera", style: .default, handler: {
(alert: UIAlertAction!) -> Void in
print("CameraOption")
self.getFromCamera()
})
//
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: {
(alert: UIAlertAction!) -> Void in
print("Cancel")
})
// 4
optionMenu.addAction(albumAction)
optionMenu.addAction(cameraAction)
optionMenu.addAction(cancelAction)
// 5
self.present(optionMenu, animated: true, completion: nil)
}
func getFromReceipts() {
print("GetFromReceipts")
let cameraPicker = UIImagePickerController()
cameraPicker.delegate = self
cameraPicker.sourceType = .photoLibrary
self.present(cameraPicker, animated: true, completion: nil )
}
func getFromCamera() {
print("GetFromCamera")
let cameraPicker = UIImagePickerController()
cameraPicker.delegate = self
cameraPicker.sourceType = .camera
self.present(cameraPicker, animated: true, completion: nil )
}
//Number of Views
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.Receipts.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtindexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Receipts, for: indexPath as IndexPath) as? PhotoCell
cell?.imageView.image = self.Receipts[indexPath.row]
return cell!
}
}
The error maybe comes from not requesting the authorization to load an photo from the camera or the photo library. So you need to request for authorization before opening the camera like:
https://developer.apple.com/documentation/photos/phphotolibrary/1620736-requestauthorization
I guess you'r not requesting Photo/Camera authorizations.
You have to add the below keys to Info.plist.
Camera permission :
<key>NSCameraUsageDescription</key>
<string> ${PRODUCT_NAME} Camera Usage< </string>
For Photo Library, you will want this one to allow app user to browse the photo library.
<key>NSPhotoLibraryUsageDescription</key>
<string>${PRODUCT_NAME} Photo Usage</string>
And in you'r source code, to ask permission for the photo/camera you need to add this code (Swift 3):
PHPhotoLibrary.requestAuthorization({
(newStatus) in
if newStatus == PHAuthorizationStatus.authorized {
/* do stuff here */
}
})