I am using Eureka forms in my project.
I have a PushRow that presents the default SelectorViewController with a list of options. In the pushed view, I have added a rightBarButtonItem that points to locationSelectorAddButton ... on click, this brings up a UIAlertController that should allow users to add options to this pushed controller.
Is it possible, without creating my own custom selector controller, to refresh the current controller with the newly saved options from UserDefaults?
let defaults = UserDefaults.standard
func setupForm() {
form
+++ PushRow<String>(K.SESSIONFIELD.location) {
$0.title = K.SESSIONFIELD.location
$0.options = defaults.array(forKey: K.SESSIONFIELD.location) as? [String]
$0.value = sessionResult?.sessionLocation ?? $0.options?.first
}
.onPresent { from, to in
to.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(named:"add_20pt"), style: UIBarButtonItemStyle.plain, target: from, action: #selector(self.locationSelectorAddButton(_:)))
}
}
the locationSelectorAddButton is implemented as follows:
#objc func locationSelectorAddButton(_ sender: UIBarButtonItem) {
var textField = UITextField()
let alert = UIAlertController(title: "Add New Location", message: "", preferredStyle: .alert)
let action = UIAlertAction(title: "Add", style: .default) { (action) in
var locArray = self.defaults.array(forKey: K.SESSIONFIELD.location) as? [String]
locArray?.append(textField.text!)
self.defaults.set(locArray, forKey: K.SESSIONFIELD.location)
///TODO: somehow refresh the pushed view controller here!!
// self.form.rowBy(tag: K.SESSIONFIELD.location)?.reload()
// print("this is the list of locations currently ...\(locArray)")
// self.tableView.reloadData()
///
}
alert.addTextField { (alertTextField) in
alertTextField.placeholder = "Location name ..."
textField = alertTextField
}
alert.addAction(action)
present(alert, animated:true, completion:nil)
}
Push row is not reloaded by using tag property. You need to implement the method
cellUpdate { cell, row in
row.options = (assign value here for updated options)
}
Related
I am trying to create a share sheet to share a Text, it was working fine in iOS 14 but in iOS 15 it tells me that
'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a
relevant window scene instead.
how can I make it work on iOS 15 with SwiftUI
Button {
let TextoCompartido = "Hola 😀 "
let AV = UIActivityViewController(activityItems: [TextoCompartido], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(AV, animated: true, completion: nil)
}
I think you would be best served using SwiftUI APIs directly. Generally, I would follow these steps.
Create SwiftUI View named ActivityView that adheres to UIViewControllerRepresentable. This will allow you to bring UIActivityViewController to SwiftUI.
Create an Identifiable struct to contain the text you'd like to display in the ActivityView. Making this type will allow you to use the SwiftUI sheet API and leverage SwiftUI state to tell the app when a new ActivityView to be shown.
Create an optional #State variable that will hold on to your Identifiable text construct. When this variable changes, the sheet API will perform the callback.
When the button is tapped, update the state of the variable set in step 3.
Use the sheet API to create an ActivityView which will be presented to your user.
The code below should help get you started.
import UIKit
import SwiftUI
// 1. Activity View
struct ActivityView: UIViewControllerRepresentable {
let text: String
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
return UIActivityViewController(activityItems: [text], applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>) {}
}
// 2. Share Text
struct ShareText: Identifiable {
let id = UUID()
let text: String
}
struct ContentView: View {
// 3. Share Text State
#State var shareText: ShareText?
var body: some View {
VStack {
Button("Show Activity View") {
// 4. New Identifiable Share Text
shareText = ShareText(text: "Hola 😀")
}
.padding()
}
// 5. Sheet to display Share Text
.sheet(item: $shareText) { shareText in
ActivityView(text: shareText.text)
}
}
}
For the future, iOS 16 will have the ShareLink view which works like this:
Gallery(...)
.toolbar {
ShareLink(item: image, preview: SharePreview("Birthday Effects"))
}
Source: https://developer.apple.com/videos/play/wwdc2022/10052/
Time code offset: 25 minutes 28 seconds
To avoid warning, change the way you retrieve the window scene.
Do the following:
Button {
let TextoCompartido = "Hola 😀 "
let AV = UIActivityViewController(activityItems: [TextoCompartido], applicationActivities: nil)
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
windowScene?.keyWindow?.rootViewController?.present(AV, animated: true, completion: nil)
}
Tested in in iOS 15 with SwiftUI
func shareViaActionSheet() {
if vedioData.vedioURL != nil {
let activityVC = UIActivityViewController(activityItems: [vedioData.vedioURL as Any], applicationActivities: nil)
UIApplication.shared.currentUIWindow()?.rootViewController?.present(activityVC, animated: true, completion: nil)
}
}
To avoid iOS 15 method deprecation warning use this extension
public extension UIApplication {
func currentUIWindow() -> UIWindow? {
let connectedScenes = UIApplication.shared.connectedScenes
.filter { $0.activationState == .foregroundActive }
.compactMap { $0 as? UIWindowScene }
let window = connectedScenes.first?
.windows
.first { $0.isKeyWindow }
return window
}
}
you could try the following using the answer from: How to get rid of message " 'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead" with AdMob banner?
Note that your code works for me, but the compiler give the deprecation warning.
public extension UIApplication {
func currentUIWindow() -> UIWindow? {
let connectedScenes = UIApplication.shared.connectedScenes
.filter({
$0.activationState == .foregroundActive})
.compactMap({$0 as? UIWindowScene})
let window = connectedScenes.first?
.windows
.first { $0.isKeyWindow }
return window
}
}
struct ContentView: View {
let TextoCompartido = "Hola 😀 "
var body: some View {
Button(action: {
let AV = UIActivityViewController(activityItems: [TextoCompartido], applicationActivities: nil)
UIApplication.shared.currentUIWindow()?.rootViewController?.present(AV, animated: true, completion: nil)
// This works for me, but the compiler give the deprecation warning
// UIApplication.shared.windows.first?.rootViewController?.present(AV, animated: true, completion: nil)
}) {
Text("Hola click me")
}
}
}
I'm currently struggling with getting my TableView to update after I finish performing some functions called in viewDidLoad and viewDidAppear. I tried using self.tableView.reloadData() at the end of my viewDidLoad but it didn't work and upon reloading the tab, the app would crash.
Here is some of my code (I'm trying to fetch events from a Google Calendar and display it in a TableView). I'm trying to display an array of strings named listOfEvents and it is being populated after the tableView is already loaded.
I also tried adding self.tableView.reloadData() at the end of my fetchEvents() but it also killed my app upon reloading the tab
class CalendarViewController: UITableViewController {
var listOfEvents: [String] = []
private let kKeychainItemName = "Google Calendar API"
private let kClientID = "clientID"
// If modifying these scopes, delete your previously saved credentials by
// resetting the iOS simulator or uninstall the app.
private let scopes = [kGTLAuthScopeCalendarReadonly]
private let service = GTLServiceCalendar()
let output = UITextView()
// When the view loads, create necessary subviews
// and initialize the Google Calendar API service
override func viewDidLoad() {
super.viewDidLoad()
if let auth = GTMOAuth2ViewControllerTouch.authForGoogleFromKeychainForName(
kKeychainItemName,
clientID: kClientID,
clientSecret: nil) {
service.authorizer = auth
}
}
// When the view appears, ensure that the Google Calendar API service is authorized
// and perform API calls
override func viewDidAppear(animated: Bool) {
if let authorizer = service.authorizer,
canAuth = authorizer.canAuthorize where canAuth {
fetchEvents()
} else {
presentViewController(
createAuthController(),
animated: true,
completion: nil
)
}
}
// Construct a query and get a list of upcoming events from the user calendar
func fetchEvents() {
let query = GTLQueryCalendar.queryForEventsListWithCalendarId("primary")
query.maxResults = 10
query.timeMin = GTLDateTime(date: NSDate(), timeZone: NSTimeZone.localTimeZone())
query.singleEvents = true
query.orderBy = kGTLCalendarOrderByStartTime
service.executeQuery(
query,
delegate: self,
didFinishSelector: "displayResultWithTicket:finishedWithObject:error:"
)
}
// Display the start dates and event summaries in the UITextView
func displayResultWithTicket(
ticket: GTLServiceTicket,
finishedWithObject response : GTLCalendarEvents,
error : NSError?) {
if let error = error {
showAlert("Error", message: error.localizedDescription)
return
}
var eventString = ""
if let events = response.items() where !events.isEmpty {
for event in events as! [GTLCalendarEvent] {
let start : GTLDateTime! = event.start.dateTime ?? event.start.date
let startString = NSDateFormatter.localizedStringFromDate(
start.date,
dateStyle: .ShortStyle,
timeStyle: .ShortStyle
)
eventString += "\(startString) - \(event.summary)\n"
// An array holding all my upcoming events
listOfEvents.append("\(startString) - \(event.summary)")
print(listOfEvents)
}
} else {
eventString = "No upcoming events found."
}
output.text = eventString
self.tableView.reloadData()
}
// Creates the auth controller for authorizing access to Google Calendar API
private func createAuthController() -> GTMOAuth2ViewControllerTouch {
let scopeString = scopes.joinWithSeparator(" ")
return GTMOAuth2ViewControllerTouch(
scope: scopeString,
clientID: kClientID,
clientSecret: nil,
keychainItemName: kKeychainItemName,
delegate: self,
finishedSelector: "viewController:finishedWithAuth:error:"
)
}
// Handle completion of the authorization process, and update the Google Calendar API
// with the new credentials.
func viewController(vc : UIViewController,
finishedWithAuth authResult : GTMOAuth2Authentication, error : NSError?) {
if let error = error {
service.authorizer = nil
showAlert("Authentication Error", message: error.localizedDescription)
return
}
service.authorizer = authResult
dismissViewControllerAnimated(true, completion: nil)
}
// Helper for showing an alert
func showAlert(title : String, message: String) {
let alert = UIAlertController(
title: title,
message: message,
preferredStyle: UIAlertControllerStyle.Alert
)
let ok = UIAlertAction(
title: "OK",
style: UIAlertActionStyle.Default,
handler: nil
)
alert.addAction(ok)
presentViewController(alert, animated: true, completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(self.listOfEvents.count)
return self.listOfEvents.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("Events Cell", forIndexPath: indexPath) as UITableViewCell
var event = ""
event = listOfEvents[indexPath.row]
cell.textLabel?.text = event
return cell
}
}
I would appreciate any help and insight :-) Thanks so much!
After output.text = eventString, you should reload the tableview.
I'm trying to make location service app and i have the following code so when the user goes to that view controller he will get an alert of getting the current location.
This is the code
override func viewDidLoad() {
super.viewDidLoad()
// 1. status is not determined
if CLLocationManager.authorizationStatus() == .NotDetermined {
locationManager.requestAlwaysAuthorization()
}
// 2. authorization were denied
else if CLLocationManager.authorizationStatus() == .Denied {
SwiftSpinner.hide()
let alert = UIAlertController(title: "Error with Your Location" , message: "Location services were previously denied. Please enable location services for this app in Settings.", preferredStyle: UIAlertControllerStyle.Alert)
let ok = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default) {
UIAlertAction in
UIApplication.sharedApplication().openURL(NSURL(string: UIApplicationOpenSettingsURLString)!)
}
alert.addAction(ok)
let cancel = UIAlertAction(title: "Back", style: UIAlertActionStyle.Default) {
UIAlertAction in
self.movenav("arxiki")
}
alert.addAction(cancel)
self.presentViewController(alert, animated: true, completion: nil)
}
// 3. we do have authorization
else if CLLocationManager.authorizationStatus() == .AuthorizedAlways {
locationManager.startUpdatingLocation()
}
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.eventsTable.backgroundColor = UIColor.lightGrayColor()
// self.locationManager.requestAlwaysAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}
}
My question is the following.
If the user pushes "Do not authorise" How can i get his option so i can send him back to the previous view controller or to alert him with the message that i have?
In order to catch the user selection you need to declare a CLLocationManager object and implement its delegate (CLLocationManagerDelegate) and use the following method for catching it.
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
if status == .Denied || status == .NotDetermined{
// User selected Not authorized
}
}
I assume you have already configured the info.plist with the suitable locations parameters.
Hope it helps!
I'm using a WKWebView in a Mac OS X application. I want to override the contextual menu that appears when the user Control + clicks or right clicks in the WKWebView, but I cannot find a way to accomplish this.
It should be noted that the context menu changes depending on the state of the WKWebView and what element is under the mouse when the context menu is invoked. For example, the context menu only has a single "Reload" item when the mouse is over an "empty" part of the content, whereas right clicking a link presents the options "Open Link", "Open Link In New Window", and so on. It would be helpful to have granular control over these different menus if possible.
The older WebUIDelegate provides the - webView:contextMenuItemsForElement:defaultMenuItems:
method that allows you to customize the context menu for WebView instances; I'm essentially looking for the analog to this method for WKWebView, or any way to duplicate the functionality.
You can do this by intercepting the contextmenu event in your javascript, reporting the event back to your OSX container through a scriptMessageHandler, then popping up a menu from OSX. You can pass context back through the body field of the script message to show an appropriate menu, or use a different handler for each one.
Setting up callback handler in Objective C:
WKUserContentController *contentController = [[WKUserContentController alloc]init];
[contentController addScriptMessageHandler:self name:#"callbackHandler"];
config.userContentController = contentController;
self.mainWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:config];
Javascript code using jquery:
$(nodeId).on("contextmenu", function (evt) {
window.webkit.messageHandlers.callbackHandler.postMessage({body: "..."});
evt.preventDefault();
});
Responding to it from Objective C:
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:#"callbackHandler"]) {
[self popupMenu:message.body];
}
}
-(void)popupMenu:(NSString *)context {
NSMenu *theMenu = [[NSMenu alloc] initWithTitle:#"Context Menu"];
[theMenu insertItemWithTitle:#"Beep" action:#selector(beep:) keyEquivalent:#"" atIndex:0];
[theMenu insertItemWithTitle:#"Honk" action:#selector(honk:) keyEquivalent:#"" atIndex:1];
[theMenu popUpMenuPositioningItem:theMenu.itemArray[0] atLocation:NSPointFromCGPoint(CGPointMake(0,0)) inView:self.view];
}
-(void)beep:(id)val {
NSLog(#"got beep %#", val);
}
-(void)honk:(id)val {
NSLog(#"got honk %#", val);
}
You can intercept context menu items of the WKWebView class by subclassing it and implementing the willOpenMenu method like this:
class MyWebView: WKWebView {
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for menuItem in menu.items {
if menuItem.identifier?.rawValue == "WKMenuItemIdentifierDownloadImage" ||
menuItem.identifier?.rawValue == "WKMenuItemIdentifierDownloadLinkedFile" {
menuItem.action = #selector(menuClick(_:))
menuItem.target = self
}
}
}
#objc func menuClick(_ sender: AnyObject) {
if let menuItem = sender as? NSMenuItem {
Swift.print("Menu \(menuItem.title) clicked")
}
}
}
Instead of this you can also simply hide the menu items with menuItem.isHidden = true
Detecting the chosen menu item is one thing, but knowing what the user actually clicked in the WKWebView control is the next challenge :)
It's also possible to add new menu items to the menu.items array.
Objective C solution. The best solution is to subclass WKWebView and intercept mouse clicks. It works great.
#implementation WKReportWebView
// Ctrl+click seems to send this not rightMouse
-(void)mouseDown:(NSEvent *)event
{
if(event.modifierFlags & NSEventModifierFlagControl)
return [self rightMouseDown:event];
[super mouseDown:event]; // Catch scrollbar mouse events
}
-(void)rightMouseDown:(NSEvent *)theEvent
{
NSMenu *rightClickMenu = [[NSMenu alloc] initWithTitle:#"Print Menu"];
[rightClickMenu insertItemWithTitle:NSLocalizedString(#"Print", nil) action:#selector(print:) keyEquivalent:#"" atIndex:0];
[NSMenu popUpContextMenu:rightClickMenu withEvent:theEvent forView:self];
}
#end
This answer builds on the excellent answers in this thread.
The challenges in working with the WKWebView's context menu are:
It can only be manipulated in a subclass of WKWebView
WebKit does not expose any information about the HTML element that the user right-clicked on. Thus, information about the element must be intercepted in JavaScript and plumbed back into Swift.
Intercepting and finding information about the element the user clicked on happens by injecting JavaScript into the page prior to rendering, and then by establishing a callback into Swift. Here is the class that I wrote to do this. It works on the WKWebView's configuration object. It also assumes that there is only one context menu available at a time:
class GlobalScriptMessageHandler: NSObject, WKScriptMessageHandler {
public private(set) static var instance = GlobalScriptMessageHandler()
public private(set) var contextMenu_nodeName: String?
public private(set) var contextMenu_nodeId: String?
public private(set) var contextMenu_hrefNodeName: String?
public private(set) var contextMenu_hrefNodeId: String?
public private(set) var contextMenu_href: String?
static private var WHOLE_PAGE_SCRIPT = """
window.oncontextmenu = (event) => {
var target = event.target
var href = target.href
var parentElement = target
while (href == null && parentElement.parentElement != null) {
parentElement = parentElement.parentElement
href = parentElement.href
}
if (href == null) {
parentElement = null;
}
window.webkit.messageHandlers.oncontextmenu.postMessage({
nodeName: target.nodeName,
id: target.id,
hrefNodeName: parentElement?.nodeName,
hrefId: parentElement?.id,
href
});
}
"""
private override init() {
super.init()
}
public func ensureHandles(configuration: WKWebViewConfiguration) {
var alreadyHandling = false
for userScript in configuration.userContentController.userScripts {
if userScript.source == GlobalScriptMessageHandler.WHOLE_PAGE_SCRIPT {
alreadyHandling = true
}
}
if !alreadyHandling {
let userContentController = configuration.userContentController
userContentController.add(self, name: "oncontextmenu")
let userScript = WKUserScript(source: GlobalScriptMessageHandler.WHOLE_PAGE_SCRIPT, injectionTime: .atDocumentStart, forMainFrameOnly: false)
userContentController.addUserScript(userScript)
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let body = message.body as? NSDictionary {
contextMenu_nodeName = body["nodeName"] as? String
contextMenu_nodeId = body["id"] as? String
contextMenu_hrefNodeName = body["hrefNodeName"] as? String
contextMenu_hrefNodeId = body["hrefId"] as? String
contextMenu_href = body["href"] as? String
}
}
Next, to enable this in your WKWebView, you must subclass it and call GlobalScriptMessageHandler.instance.ensureHandles in your constructor:
class WebView: WKWebView {
public var webViewDelegate: WebViewDelegate?
init() {
super.init(frame: CGRect(), configuration: WKWebViewConfiguration())
GlobalScriptMessageHandler.instance.ensureHandles(configuration: self.configuration)
}
Finally, (as other answers have pointed out,) you override the context menu handler. In this case I changed the action in target for the "Open Link" menu item. You can change them as you see fit:
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for index in 0...(menu.items.count - 1) {
let menuItem = menu.items[index]
if menuItem.identifier?.rawValue == "WKMenuItemIdentifierOpenLink" {
menuItem.action = #selector(openLink(_:))
menuItem.target = self
And then, in your method to handle the menu item, use GlobalScriptMessageHandler.instance.contextMenu_href to get the URL that the user right-clicked:
#objc func openLink(_ sender: AnyObject) {
if let url = GlobalScriptMessageHandler.instance.contextMenu_href {
let url = URL(string: url)!
self.load(URLRequest(url: url))
}
}
Following the answers already given I was able to modify the menu and also found a way get the URL that was selected by the user. I suppose this approach can also be used to get an image or any other similar content selected, and I'm hoping this can help other folks.
This is written using Swift 5
This approach consists on performing the action from the menu item "Copy Link", so that the URL gets copied into the paste board, then retrieving the URL from the paste board to use it on a new menu item.
Note: Retrieving the URL from the pasteboard needs to be called on an async closure, allowing time for the URL to first be copied into it.
final class WebView: WKWebView {
override func willOpenMenu(_ menu: NSMenu, with: NSEvent) {
menu.items.first { $0.identifier?.rawValue == "WKMenuItemIdentifierCopyLink" }.map {
guard let action = $0.action else { return }
NSApp.sendAction(action, to: $0.target, from: $0)
DispatchQueue.main.async { [weak self] in
let newTab = NSMenuItem(title: "Open Link in New Tab", action: #selector(self?.openInNewTab), keyEquivalent: "")
newTab.target = self
newTab.representedObject = NSPasteboard.general.string(forType: .string)
menu.items.append(newTab)
}
}
}
#objc private func openInNewTab(_ item: NSMenuItem) {
print(item.representedObject as? String)
}
}
I need to get the text from the text fields in my alert view when the Input button is pressed.
func inputMsg() {
var points = ""
var outOf = ""
var alertController = UIAlertController(title: "Input View", message: "Please Input Your Grade", preferredStyle: UIAlertControllerStyle.Alert)
let actionCancle = UIAlertAction(title: "Cancle", style: UIAlertActionStyle.Cancel) { ACTION in
println("Cacle")
}
let actionInput = UIAlertAction(title: "Input", style: UIAlertActionStyle.Default) { ACTION in
println("Input")
println(points)
println(outOf)
}
alertController.addAction(actionCancle)
alertController.addAction(actionInput)
alertController.addTextFieldWithConfigurationHandler({(txtField: UITextField!) in
txtField.placeholder = "I got"
txtField.keyboardType = UIKeyboardType.NumberPad
points = txtField.text
})
alertController.addTextFieldWithConfigurationHandler({(txtField: UITextField!) in
txtField.placeholder = "Out Of"
txtField.keyboardType = UIKeyboardType.NumberPad
outOf = txtField.text
})
presentViewController(alertController, animated: true, completion: nil)
}
As requested here is an implementation solution.
alertController.addAction(UIAlertAction(title: "Submit", style: UIAlertActionStyle.Default,handler: {
(alert: UIAlertAction!) in
if let textField = alertController.textFields?.first as? UITextField{
println(textField.text)
}
}))
As stated above, the alertController has a property called textFields. You can conditionally unwrap that property to safely access a text field if you have added one. In this case since there is only one text field I just did the unwrap using the first property. Hope it helps.
The UIAlertController has a textFields property. That's its text fields. Any of your handlers can examine it and thus can get the text from any of the text fields.