I've been trying to figure out how to integrate an ASWebAuthenticationSession (to perform login via Oauth) with SwiftUI. I can't find any documentation on the same online anywhere and was wondering if someone with more SwiftUI and iOS dev experience could tell me how I can achieve something along the lines. I currently need a system for someone to click the Login button which then opens a ASWebAuthSession and allows the user to login before redirecting them back to my app and loading another SwiftUI view.
I have in my ContentView one button whos calls this function :
func getAuthTokenWithWebLogin() {
let authURL = URL(string: "https://test-login.blabla.no/connect/authorize?scope=openid%20profile%20AppFramework%20offline_access&response_type=code&client_id=<blalblalba>&redirect_uri=https://integration-partner/post-login")
let callbackUrlScheme = "no.blabla:/oauthdirect"
webAuthSession = ASWebAuthenticationSession.init(url: authURL!, callbackURLScheme: callbackUrlScheme, completionHandler: { (callBack:URL?, error:Error?) in
// handle auth response
guard error == nil, let successURL = callBack else {
return
}
let oauthToken = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == "code"}).first
// Do what you now that you've got the token, or use the callBack URL
print(oauthToken ?? "No OAuth Token")
})
// Kick it off
webAuthSession?.start()
}
But I get this error:
Cannot start ASWebAuthenticationSession without providing presentation
context. Set presentationContextProvider before calling -start.
How should I do this in SwiftUI? Any examples would be fantastic!
With .webAuthenticationSession(isPresented:content) modifier in BetterSafariView, you can easily use ASWebAuthenticationSession in SwiftUI. It handles the work related to providing presentation context.
import SwiftUI
import BetterSafariView
struct ContentView: View {
#State private var startingWebAuthenticationSession = false
var body: some View {
Button("Start WebAuthenticationSession") {
self.startingWebAuthenticationSession = true
}
.webAuthenticationSession(isPresented: $startingWebAuthenticationSession) {
WebAuthenticationSession(
url: URL(string: "https://github.com/login/oauth/authorize?client_id=\(clientID)")!,
callbackURLScheme: "myapp"
) { callbackURL, error in
print(callbackURL, error)
}
}
}
}
Related
I am trying to coordinate my deep link with push notifications so they both process my custom url scheme in the same manner and navigate to the appropriate view. The challenge seems to be with push notifications and how to process the link passed, through an apn from Azure Notification Hubs, using the same #EnvironmentObject that the onOpenUrl uses without breaking the SwiftUI paradigm and using a singleton.
Here is how I trigger the notification on my simulator, which works fine and navigates me to the appropriate view:
xcrun simctl openurl booted "myapp://user/details/123456"
Which triggers this the onOpenUrl in this code:
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(sessionInfo)
.onOpenURL { url in
print("onOpenURL: \(url)")
if sessionInfo.processDeepLink(url: url) {
print("deep link TRUE")
} else {
print("deep link FALSE")
}
}
}
}
And all my DeepLinks work just as desired. I wanted to trigger them from a notification so I created an apns file with the same link that worked using xcrun:
{
"aps": {
"alert": { // alert data },
"badge": 1,
"link_url":"myapp://user/details/123456"
}
}
and pushed it to the simulator like this:
xcrun simctl push xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx com.myco.myapp test.apn
How do I reference my object from the AppDelegate which gets the message:
func notificationHub(_ notificationHub: MSNotificationHub!, didReceivePushNotification message: MSNotificationHubMessage!) {
print("notificationHub...")
let userInfo = ["message": message!]
print("user: ")
NotificationCenter.default.post(name: NSNotification.Name("MessageReceived"), object: nil, userInfo: userInfo)
if (UIApplication.shared.applicationState == .background) {
print("Notification received in the background")
} else {
print("Notification received in the foreground")
}
UIApplication.shared.applicationIconBadgeNumber = 4
}
I looked at this post, but couldn't relate the components to my app, possibly due to the NotificationHub part of it. I also saw this post, but again didn't know how to connect it.
I saw this post and it looks like push notification and deep linking are two different things. I could use the same logic if I could access the SessionInfo object from the AppDelegate. I'm concerned about messing around in there given I'm new to iOS development. Here is what I'm using in the App:
#StateObject var sessionInfo: SessionInfo = SessionInfo()
This post seems to cover it but I'm still having trouble. I don't know what this means:
static let shared = AppState()
And my SessionInfo is decorated with #MainActor. When I access it from other places I use:
#EnvironmentObject var sessionInfo: SessionInfo
I also saw this post, which there was no selected answer, but the one which did exist seemed to recommend making the AppDelegate and EnvrionmentObject and push it into the ContentView. I think what I really need is the AppDelegate when the notification arrives to update something shared/published to the SessionInfo object so the url is parsed and the navigation kicked off. This seems backwards to me.
This post makes the AppDelegate an ObservableObject with a property which is published and makes the AppDelegate an EnvrionmentObject, so when the value is updated, it's published. If it were the navigation link/object that would work but something would still need to process it and it would not make sense for the onOpenUrl to use the AppDelegate, so again I think this is backwards.
If I did follow the post where there is a static SessionInfo object in the SessionInfo class, singleton, that means I would need to remove the #EnvironmentObject var sessionInfo: SessionInfo from the ContentView and the .environmentObject(sessionInfo) on the main View I am using I think and instead instantiate the shared object in each view where it is used. Right? It seems like I followed this whole #EnvrionmentObject, #StateObject, #MainActor paradigm and would have to abandon it. I'm not sure if that is right or what the tradeoffs are.
Most recently this post seems to be pretty in-depth, but introduces a new element, UNUserNotificationCenter, which I heard referenced in this youtube video.
This article was very helpful for the notification part.
Azure NotificationHubs the message info is in message.userInfo["aps"] vs userInfo["aps"] in the example or most places I have seen it. Not much documentation on MSNotificationHubMessage:
func notificationHub(_ notificationHub: MSNotificationHub, didReceivePushNotification message: MSNotificationHubMessage) {
print("notificationHub...")
let title = message.title ?? ""
let body = message.body ?? ""
print("title: \(title)")
print("body: \(body)")
let userInfo = ["message": message]
NotificationCenter.default.post(name: NSNotification.Name("MessageReceived"), object: nil, userInfo: userInfo)
guard let aps = message.userInfo["aps"] as? [String: AnyObject] else {
return
}
...
}
Second, this post provided the answer which I adapted for my project:
final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, MSNotificationHubDelegate {
var navMgr: NavigationManager = NavigationManager()
...
}
and
#UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(sessionInfo)
.environmentObject(appDelegate.navMgr)
.onOpenURL { url in
print("onOpenURL: \(url)")
if sessionInfo.processDeepLink(url: url) {
print("deep link TRUE")
} else {
print("deep link FALSE")
}
}
}
}
I am attempting to access a user's custom claim on my iOS client in order to govern UI flow at login, following the example in the Firebase documentation. However, when I try to integrate the coding example provided it causes multiple "Type 'Void' cannot conform to 'View'" errors in Xcode and does not compile.
In my code below, I've simply added a User object on which getIDTokenResult is called and I display a LoginView if that user is nil.
Why am I receiving these errors?
struct ContentView: View {
#ObservedObject private var authenticationService = AuthenticationService()
var body: some View {
if let user = authenticationService.user {
user.getIDTokenResult(completion: { (result, error) in
guard let admin = result?.claims?["admin"] as? NSNumber else {
// Show regular UI.
ClientHomeView()
return
}
if admin.boolValue {
// Show admin UI.
AdminHomeView()
} else {
// Show regular UI.
ClientHomeView()
}
})
}
else {
LoginView()
}
}
}
I have an app where users can sign up and login using Firebase. However, I can not seem to alert the user of any errors in the view.
First we have a UserStore which is a ObservableObject and is initialised as an EnvironmentObject when setting up the view in the SceneDelegate.
let appView = AppView().environmentObject(userStore)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: appView)
self.window = window
window.makeKeyAndVisible()
}
Then we sign up or login to the View like so.
In View
self.userStore.logIn(email: self.email, password: self.password)
self.isLoggingIn = true
if self.userStore.failedToLogin {
self.isLoggingIn = false
self.alertTitle = "There seems to be a problem"
self.alertBody = self.userStore.errorMessage
self.showingAlert = true
}
Then the actual method should update the UserStore property values which then update the view and display an alert, however, this is not the case.
SignIn
// Session Properties
#Published var isLoggedIn = false {didSet{didChange.send() }}
#Published var isNewUser = false {didSet{didChange.send() }}
#Published var failedToCreateAccount = false {didSet{didChange.send() }}
#Published var failedToLogin = false {didSet{didChange.send() }}
#Published var errorMessage = "" {didSet{didChange.send() }}
init () {
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
self.session = user
self.isLoggedIn = true
//Change isNewUser is user document exists?
} else {
self.session = nil
self.isLoggedIn = false
}
}
}
func logIn(email: String, password: String) {
Auth.auth().signIn(withEmail: email, password: password) { [weak self] user, error in
print("Signed In")
if(error != nil) {
print("Failed to login")
self!.failedToLogin = true
self!.errorMessage = ("\(String(describing: error))")
print(error!)
return
} else if error == nil {
print("Success Logging In")
}
}
}
The AppView determines which view is loaded depending if the user is logged in.
AppView
if !userStore.isLoggedIn {
LoginView().transition(.opacity)
}
if userStore.isLoggedIn {
ContentView().transition(.opacity)
}
Atm error messages are not shown; the login view is also shown shortly before the main view.
How can I correctly display error messages in the view ?
The Firebase APIs are asynchronous, simply because they access a remote system, across the internet, which takes a little time. The same applies for accessing the local disk, by the way. This blog post explains this in more detail.
Consequently, userStore.login is an asynchronous process. I.e. the call to if self.userStore.failedToLogin is executed before userStore.login returns. Thus, userStore.failedToLogin is still false, and the code in the conditional statement will never be executed.
There are two ways around this:
Implement a trailing closure on userStore.logIn, and move the code which displays the error into the closure
Make userStore.failedToLogin a publisher and subscribe the visibility of your alert to it
Use Combine Firebase: https://github.com/rever-ai/CombineFirebase
This is combine-swiftui wrapper around firebase api, so you can use swift-ui publisher pattern for firebase.
I'm getting info from an API using the following function where I pass in a string of a word. Sometimes the word doesn't available in the API if it doesn't available I generate a new word and try that one.
The problem is because this is an asynchronous function when I launch the page where the value from the API appears it is sometimes empty because the function is still running in the background trying to generate a word that exists in the API.
How can I make sure the page launches only when the data been received from the api ?
static func wordDefin (word : String, completion: #escaping (_ def: String )->(String)) {
let wordEncoded = word.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let uri = URL(string:"https://dictapi.lexicala.com/search?source=global&language=he&morph=false&text=" + wordEncoded! )
if let unwrappedURL = uri {
var request = URLRequest(url: unwrappedURL);request.addValue("Basic bmV0YXlhbWluOk5ldGF5YW1pbjg5Kg==", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
do {
if let data = data {
let decoder = JSONDecoder()
let empty = try decoder.decode(Empty.self, from: data)
if (empty.results?.isEmpty)!{
print("oops looks like the word :" + word)
game.wordsList.removeAll(where: { ($0) == game.word })
game.floffWords.removeAll(where: { ($0) == game.word })
helper.newGame()
} else {
let definition = empty.results?[0].senses?[0].definition
_ = completion(definition ?? "test")
return
}
}
}
catch {
print("connection")
print(error)
}
}
dataTask.resume()
}
}
You can't stop a view controller from "launching" itself (except not to push/present/show it at all). Once you push/present/show it, its lifecycle cannot—and should not—be stopped. Therefore, it's your responsibility to load the appropriate UI for the "loading state", which may be a blank view controller with a loading spinner. You can do this however you want, including loading the full UI with .isHidden = true set for all view objects. The idea is to do as much pre-loading of the UI as possible while the database is working in the background so that when the data is ready, you can display the full UI with as little work as possible.
What I'd suggest is after you've loaded the UI in its "loading" configuration, download the data as the final step in your flow and use a completion handler to finish the task:
override func viewDidLoad() {
super.viewDidLoad()
loadData { (result) in
// load full UI
}
}
Your data method may look something like this:
private func loadData(completion: #escaping (_ result: Result) -> Void) {
...
}
EDIT
Consider creating a data manager that operates along the following lines. Because the data manager is a class (a reference type), when you pass it forward to other view controllers, they all point to the same instance of the manager. Therefore, changes that any of the view controllers make to it are seen by the other view controllers. That means when you push a new view controller and it's time to update a label, access it from the data property. And if it's not ready, wait for the data manager to notify the view controller when it is ready.
class GameDataManager {
// stores game properties
// updates game properties
// does all thing game data
var score = 0
var word: String?
}
class MainViewController: UIViewController {
let data = GameDataManager()
override func viewDidLoad() {
super.viewDidLoad()
// when you push to another view controller, point it to the data manager
let someVC = SomeOtherViewController()
someVC.data = data
}
}
class SomeOtherViewController: UIViewController {
var data: GameDataManager?
override func viewDidLoad() {
super.viewDidLoad()
if let word = data?.word {
print(word)
}
}
}
class AnyViewController: UIViewController {
var data: GameDataManager?
}
I am using Xcode 8.3.3 and Swift 3 to develop an app for the iMac using Cocoa. My goal is to use VCgoToWebPage and display a webpage to the user. My program calls this function many times, but the only webpage I see is the last one called. How do I implement a window refresh inside this function and wait for the webpage to be fully rendered?
func VCgoToWebPage(theWebPage : String) {
let url = URL(string: theWebPage)!
let request = URLRequest(url: url)
webView.load(request)
/*The modal box allows the web pages to be seen. Without it, after a series of calls to VCgoToWebPage only the last page called is displayed. The modal box code is just for debugging and will be removed. */
let alert = NSAlert()
alert.messageText="calling EDGAR page"
alert.informativeText=theWebPage
alert.addButton(withTitle: "OK")
alert.runModal()
}
You can use navigation delegate to make sure navigation to a page is complete before trying to load another. Have your class conform to WKNavigationDelegate and set webView.navigationDelegate to that class instance.
var allRequests = [URLRequest]()
func VCgoToWebPage(theWebPage : String) {
guard let url = URL(string: theWebPage) else {
return
}
let request = URLRequest(url: url)
if webView.isLoading{
allRequests.append(request)
} else {
webView.load(request)
}
}
func webView(WKWebView, didFinish: WKNavigation!){
if let nextRequest = allRequests.first{
webView.load(nextRequest)
allRequests.removeFirst()
}
}