I'm working on a school project where I need to implement OAuth2 to get access to our student profiles and display them. The app has two views - a search bar and detailed user info on the next page. I'm fairly new to this, so any advice will be greatly appreciated.
I was mostly following this tutorial https://grokswift.com/alamofire-OAuth2/ but a lot of stuff is deprecated. I can easily get all data using postman and user credentials but when I implement OAuth2 I can't parse the code I receive. So all my functions are in ContentView (pretty sure it's not the right way, but I couldn't make it work otherwise for now). I have a search button that looks like this:
Button(
action: {self.handleAPI()},
label: {
Text("Search")
}
And a handleAPI function that calls other functions, and func startOAuth2Login() among them:
if let authURL:NSURL = NSURL(string: authPath)
{
UIApplication.shared.open(authURL as URL, options: [:], completionHandler: nil)
print("here1")
}
Everything works as expected, redirects me to the login screen, where I enter my credentials, press "Authorize" and successfully return to the app. The print statement is also visible. However, I can't find the link I receive to parse the auth code. I know it should be in AppDelegate and this is what I have for now but it's not being called.
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
ContentView.sharedInstance.processOAuthStep1Response(url: url as NSURL)
print("url: \(url)")
return true
}
processOAuthStep1Response(url) is just another function in my handleAPI main function. I also have this line in my ContentView : static let sharedInstance = ContentView().
So the question is how to call my function that will parse the code? How can I even see this code? XCode doesn't give any errors and everything works, just after "here1" nothing else is called. Thank you in advance!
Related
What I am dealing with: web page + backend (php), swift app (wkwebview + some native components).
What I am trying to achieve: I need to let the web page know my app is changing it's state, by executing some java scripts (one script for "enter background mode", another one for "enter foreground", and so on).
What is my issue: I can't handle the case of app termination. Whenever the user is killing the app by home-double-click/swipe up, ApplicationWillTerminate method from AppDelegate is being called, but it fails to execute webView.evaluateJavaScript from here, as far as I was able to understand - due to the closure / completion handler is has, for it is async by design.
All other cases are covered, works fine, and with the last one for termination I am stuck.
I am playing around with the following code:
func applicationWillTerminate(_ application: UIApplication) {
if let callback = unloadListenerCallback {
if callback == "" {
debugPrint("got null callback - stop listening")
unloadListenerCallback = nil
}
else {
jsCallbackInjection(s: callback) //executing JS callback injection into WebView
}
}
}
unloadListenerCallback is the string? variable, works as a charm in other cases (just to mention it for clarity). jsCallbackInjection is the function, also works perfectly elsewhere:
func jsCallbackInjection(s: String) {
let jscallbackname = s
HomeController.instance.webView?.evaluateJavaScript(jscallbackname) { (result, error) in
if error == nil {
print(result as Any)
print("Executed js callback: \(jscallbackname)")
} else {
print("Error is \(String(describing: error)), js callback is: \(jscallbackname)")
}
}
}
How do I made this code working, if possible? Any ideas, suggestions, tips or tricks? If not possible, any ideas on "how do I let my web counterpart know my app was terminated"? What do I do from here?
This is a cross-post from Apple Dev forum, where it is sitting for >1 week unattended.
Answering my own question:
I was not able to find a way of running JS from ApplicationWillTerminate.
However, I found a way of solving my issue, which is - instead of running JS, I am posting to my web service like that:
func applicationWillTerminate(_ application: UIApplication) {
let semaphore = DispatchSemaphore(value: 0)
//setup your request here - Alamofire in my case
DispatchQueue.global(qos: .background).async {
//make your request here
onComplete: { (response: Update) in
//handle response if needed
semaphore.signal()
},
onFail: {
//handle failure if needed
semaphore.signal()
})
}
semaphore.wait(timeout: .distantFuture)
}
This way, I am able to consistently report my app termination to the web page. I was lucky enough to have ajax already set up on the other end of a pipe, which I am just POSTing into with the simple AF request, so I don't need to struggle with JS anymore.
Basing on what I was able to find, there is NO suitable way of managing JS execution, due to
with semaphores, as soon as webview and it's methods are to be handled in main thread, you'll deadlock by using semaphore.wait()
no way I was able to find to run evaluateJavaScript synchronously
no way I was able to find to run the JS itself from JavaScriptCore, or some other way, within the same session and context
However, if someone still can contribute and provide solution, I'll be happy to accept it!
I creating this app for my church and one feature is a prayer wall. I am using firebase as my database integrated with google sign in. When a user post a prayer request it displays their display names amongst other things. I implemented a switch and some line of code to change the display name to anonymous if the switch is on. That works however it adds anonymous after the display name. Please see the examples below.
I am really new to swift and I am self taught this is my first app, any help would be greatly appreciated.
#IBAction func didPostPrayerRequest(_ sender: Any) {
var userInfo = Auth.auth().currentUser?.displayName
if privacyFilter.isOn {
userInfo?.append("anonymous")
}
let prayerPosted:[String: Any] = ["praydate": [".sv":"timestamp"], "prayer": prayerPostText.text!,"username":userInfo!]
prayerRef?.child("Prayers").childByAutoId().setValue(prayerPosted)
print("Any")
//Dismiss popover
presentingViewController?.dismiss(animated: true, completion: nil)
}
Screenshot
Simply overwrite the value using userInfo = "anonymous" instead of appending to it.
I have File Promises implemented in a Cocoa app that allows dragging images from a view and dropping them to folders on the machine or to apps like preview or evernote. In the past this has worked well using the NSDraggingSource delegate and 'namesOfPromisedFilesDropped' method. This method would return the drop location and would allow me to write the image data directly there. So when dropping to an application icon like preview, or within an app like evernote, the file would be written and the app would either load or the image would simply show within.
Unfortunately this method was deprecated in 10.13 so it no longer gets called in new OS versions. Instead I've switched over to using the filePromiseProvider writePromiseTo url method of the "NSFilePromiseProviderDelegate" delegate. This method gets called, and the image data is processed. I get the destination URL and attempt to write the image data to this location. This works perfectly fine when simply dragging to folders on the Mac. But, when dragging to other app icons like Preview.app, or directly to a folder in Evernote, I get an error 'Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"'.
I've attempted this using full URLs, and, URL paths. Regardless of either, when dragging to other apps it simply will not allow the drop or creation of the file at that location.
Is there some entitlement that may be missing? I've even attempted the Apple source code found here with the same error: File Promises Source
Here is the code I'm using now that's returning the error with inability to write to outside app locations. This only works for dragging to folders on the computer.
extension DragDropContainerView: NSFilePromiseProviderDelegate {
internal func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
let fileName = NameFormatter.getFormattedNameFromDate() + "." + fileType.getFileType().typeIdentifier
return fileName
}
internal func operationQueue(for filePromiseProvider: NSFilePromiseProvider) -> OperationQueue {
return workQueue
}
internal func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: #escaping (Error?) -> Void) {
if let snapshot = filePromiseProvider.userInfo as? SnapshotItem {
if let data = snapshot.representationData {
do {
try data.write(to: url)
completionHandler(nil)
} catch {
completionHandler(error)
}
}
}
}
}
Any help on this issue would be great. Ultimately, I simply want to be able to drag the image to an app and have that app accept the drag. This used to work but no longer does.
Update 1:
After extensive experimentation I've managed to find a 'solution' that works. I don't see why this works but some how this ultimately kicks off a flow that fires the old 'deprecated' method.
I create the dragged item;
let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardWriter(forImageCanvas: self))
draggingItem.setDraggingFrame(screenshotImageView.frame, contents: draggingImage)
beginDraggingSession(with: [draggingItem], event: event, source: self)
This calls the below method;
private func pasteboardWriter(forImageCanvas imageCanvas: DragDropContainerView) -> NSPasteboardWriting {
let provider = FilePromiseProvider(fileType: kUTTypeJPEG as String, delegate: self)
provider.userInfo = imageCanvas.snapshotItem
return provider
}
This sets up the custom file promise session using the subclass below. As you can see, I'm not handling any logic here and it seems to be doing very little or nothing. As an added test to verify it's not really doing much, I'm setting the pasteboard type to 'audio'. I'm dragging an image not audio but it still works.
public class FilePromiseProvider : NSFilePromiseProvider {
public override func writableTypes(for pasteboard: NSPasteboard)
-> [NSPasteboard.PasteboardType] {
return [kUTTypeAudio as NSPasteboard.PasteboardType]
}
public override func writingOptions(forType type: NSPasteboard.PasteboardType,
pasteboard: NSPasteboard)
-> NSPasteboard.WritingOptions {
return super.writingOptions(forType: type, pasteboard: pasteboard)
}
}
So long at the above methods are implemented it apparently kicks off a flow that ultimately calls the 'deprecated' method below. This method works every time perfectly in Mojave showing that there must be an issue with the NSFilePromises API. If this method works, the file promises API should work the same but it does not;
override func namesOfPromisedFilesDropped(atDestination dropDestination: URL) -> [String]?
Once this method gets called everything works perfectly in Mojave for drags to app icons in the dock and directly into applications like Evernote returning the app to 100% drag drop functionality as it used to work in previous OS versions!
My file promises delegate is still in place but looks like the below. As you can see it's not doing anything any longer. However it's still required.
extension DragDropContainerView: NSFilePromiseProviderDelegate {
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
return ""
}
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: #escaping (Error?) -> Void) {
}
}
Any comments on this 'solution' would be great. Any suggestions on how to do this better would also be welcomed. Please note, that according to Apple, there "is no guarantee that the file will be written in time" using the File Promises API. But with the above, the old deprecated method is somehow called in Mojave and works flawlessly every time.
Thanks
I am validating urls from NSSavePanel using the delegate's panel(_:validate) method, throwing error in case of invalid url. In such case the NSSavePanel presents an alert, which I want to customize (meaning present some human readable description) depending on the error thrown, keeping the save panel window open and then letting you choose another path.
LocalizedError works just fine when not using App Sandbox but in a sandboxed app the getter for error description is never called and the message in the alert is generic "Operation couldn't be completed. (#yourErrorType)", which I guess is somehow caused by the different inheritance chain for sandboxed NSSavePanels.
I am struggling figuring a way around this - is it possible to customize the alert somehow while still keeping the app sandboxed?
Addendum: Permissions for User Selected File => r/w. Running the following example produces different alerts with/without sandbox.
func runSavePanel()
{
let panel = NSSavePanel()
let delegate = SavePanelDelegate()
panel.delegate = delegate
_ = panel.runModal()
}
class SavePanelDelegate: NSObject, NSOpenSavePanelDelegate {
func panel(_ sender: Any, validate url: URL) throws {
throw CustomError.whatever
}
}
enum CustomError: LocalizedError {
case whatever
var errorDescription: String? {
get {
return "my description"
}
}
}
So, after a bit of further digging I can tell the solution of the riddle finally although I can only guess the reasons why it was made tricky by Apple. Apparently NSError exclusively needs to be used. The customization has to be done in userInfo, say
let userInfo = [NSLocalizedDescriptionKey: "yourLocalizedDescription", NSLocalizedRecoverySuggestionErrorKey: "yourSuggestion"]
throw NSError(domain: "whatever", code: 0, userInfo: userInfo)
etc. By the way subclassing NSError doesn't work, the Sandbox will just happily ignore you :)
I am building an iOS app and I just finished my login/register part ( requesting a sails.js rest Api)
At the moment I have 2 view controllers with duplicate code because i issue the rest calls on register/login button event listener of each class and there is a lot of similar code I can refactor.
What I want to do is to create a singleton called ApiManager that will contain all the calls that I need. (And the futur ones )
The problem is that with async calls I can't create a function func login(username,password) that will return data so I can store them and prepareforsegue.
What is the simple/proper way to achieve that correctly? Which means call ApiManager.myFunction and using the result wherever it's needed ( filling a tableview for data, initiating a segue for login or register with succes ) and to make this function reusable in another view controller even if it is for another usage. I am using swift.
EDIT : Here is how i did it so i hope it will help you
The function executing the rest call :
func login(#username: String, password: String, resultCallback: (finalresult: UserModel!,finalerror:String!) -> Void) {
Alamofire.request(.POST, AppConfiguration.ApiConfiguration.apiDomain+"/login", parameters: ["username": username,"password": password], encoding: .JSON)
.responseJSON { request, response, data, error in
if let anError = error
{
resultCallback(finalresult: nil,finalerror:anError.localizedDescription)
}else if(response!.statusCode == 200){
var user:UserModel = self.unserializeAuth(data!)//just processing the json using SwiftyJSON to get a easy to use object.
resultCallback(finalresult: user,finalerror:nil)
}else{
resultCallback(finalresult: nil,finalerror:"Username/Password incorrect!")
}
}.responseString{ (request, response, stringResponse, error) in
// print response as string for debugging, testing, etc.
println(stringResponse)
}
}
And this is how i call this function from my ViewController :
#IBAction func onLoginTapped(sender: AnyObject) {//When my user tap the login button
let username = loginInput.text;//taking the content of inputs
let password = passwordInput.text;
ApiManager.sharedInstance.login(username:username,password:password){
[unowned self] finalresult,finalerror in
if(finalresult !== nil){//if result is not null login is successful and we can now store the user in the singleton
ApiManager.sharedInstance.current_user=finalresult
self.performSegueWithIdentifier("showAfterLogin", sender: nil)//enter the actual app and leave the login process
}else{
self.displayAlert("Error!", message: finalerror)//it is basically launching a popup to the user telling him why it didnt work
}
}
}
Almost all of my apps end up with a Server class which is the only one that knows how to communicate with the server. It makes the call, parses the result into a Swift struct and returns it. Most of my servers return json so I use SwiftyJSON, but you can do whatever you want.
The point is, that since this is the only class that knows about server communication, if I need to change the library being used to do the communication (AFNetworking 1 vs 2 vs Parse, vs whatever) this is the only class I need to touch.
class Server {
static let instance = Server()
func loginWithUsername(username: String, password: String, resultCallback: (result: Either<User, NSError>) -> Void) {
// if login is successful call
resultCallback(result: .Left(self.user!))
// otherwise call
resultCallback(result: .Right(error))
}
}
An example of use:
let server = Server.instance
SVProgressHUD.showWithStatus("Loggin In...")
server.loginWithUsername(username, password: password) { [unowned self] result in
SVProgressHUD.dismiss()
switch result {
case .Left(let user):
self.presentUserType(user.userType)
case .Right(let error):
self.warnUserWithMessage("An error occured. \(error.localizedDescription)")
}
}
If the username/password are needed for all subsequent calls, then the server object will maintain a copy of them. If the login returns a token, then the server keeps a copy of that.
QED.
I usually have utility functions in a base class shared by my view controllers and use NSNotificationCenter for reacting to the results of the requests. It can also easily be achieved through delegation (protocol & delegate.
It is mostly about perception but I find it is easier to visualize that you can, for example, start an action on one controller and react on another because the call took this long and you were not blocking navigation in your app.