How to return from the ASWebAutheticationSession completion handler back to the View?
Edit: Just for clearance this is not the original code in my project this is extremely shortened and is just for showcasing what I mean.
Here's an example of a code
struct SignInView: View {
#EnvironmentObject var signedIn: UIState
var body: some View {
let AuthenticationSession = AuthSession()
AuthenticationSession.webAuthSession.presentationContextProvider = AuthenticationSession
AuthenticationSession.webAuthSession.prefersEphemeralWebBrowserSession = true
AuthenticationSession.webAuthSession.start()
}
}
class AuthSession: NSObject, ObservableObject, ASWebAuthenticationPresentationContextProviding {
var webAuthSession = ASWebAuthenticationSession.init(
url: AuthHandler.shared.signInURL()!,
callbackURLScheme: "",
completionHandler: { (callbackURL: URL?, error: Error?) in
// check if any errors appeared
// get code from authentication
// Return to view to move on with code? (return code)
})
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return ASPresentationAnchor()
}
}
So what I'm trying to do is call the sign In process and then get back to the view with the code from the authentication to move on with it.
Could somebody tell me how this may be possible?
Thanks.
Not sure if I'm correctly understanding your question but it is normally done with publishers, commonly with the #Published wrapper, an example:
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
Button {
self.viewModel.signIn(user: "example", password: "example")
}
label: {
Text("Sign in")
}
if self.viewModel.signedIn {
Text("Successfully logged in")
}
else if let error = self.viewModel.signingError {
Text("Error while logging in: \(error.localizedDescription)")
}
}
.padding()
}
}
class ViewModel: ObservableObject {
#Published var signingStatus = SigningStatus.idle
var signedIn: Bool {
if case .success = self.signingStatus { return true }
return false
}
var signingError: Error? {
if case .failure(let error) = self.signingStatus { return error }
return nil
}
func signIn(user: String, password: String) {
self.dummyAsyncProcessWithCompletionHandler { [weak self] success in
guard let self = self else { return }
guard success else {
self.updateSigning(.failure(CustomError(errorDescription: "Login failed")))
return
}
self.updateSigning(.success)
}
}
private func updateSigning(_ status: SigningStatus) {
DispatchQueue.main.async {
self.signingStatus = status
}
}
private func dummyAsyncProcessWithCompletionHandler(completion: #escaping (Bool) -> ()) {
Task {
print("Signing in")
try await Task.sleep(nanoseconds: 500_000_000)
guard Int.random(in: 0..<9) % 2 == 0 else {
print("Error")
completion(false)
return
}
print("Success")
completion(true)
}
}
enum SigningStatus {
case idle
case success
case failure(Error)
}
struct CustomError: Error, LocalizedError {
var errorDescription: String?
}
}
I am using the MVVM architecture in Swift-UI with Combine Framework and Alamofire.
However the data is being returned to observable object and is printing means the Alamofire and published data from api layers but from obserable object its not going to view.
I print the response and its printing in publisher but not returning back to view.
Following is the observable object.
import SwiftUI
import Combine
class CountryViewModel: ObservableObject {
#Published var countryResponse:[CountryResponse] = []
#Published var isLoggedIn = false
#Published var isLoading = false
#Published var shouldNavigate = false
private var disposables: Set<AnyCancellable> = []
var loginHandler = CountryHandler()
#Published var woofUrl = ""
private var isLoadingPublisher: AnyPublisher<Bool, Never> {
loginHandler.$isLoading
.receive(on: RunLoop.main)
.map { $0 }
.eraseToAnyPublisher()
}
private var isAuthenticatedPublisher: AnyPublisher<[CountryResponse], Never> {
loginHandler.$countryResponse
.receive(on: RunLoop.main)
.map { response in
guard let response = response else {
return []
}
print(response)
return response
}
.eraseToAnyPublisher()
}
init() {
countryResponse = []
isLoadingPublisher
.receive(on: RunLoop.main)
.assign(to: \.isLoading, on: self)
.store(in: &disposables)
isAuthenticatedPublisher.map([CountryResponse].init)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] value in
guard let self = self else { return }
switch value {
case .failure:
self.countryResponse = []
case .finished:
break
}
}, receiveValue: { [weak self] weather in
guard let self = self else { return }
self.countryResponse = weather
})
.store(in: &disposables)
// isAuthenticatedPublisher
// .receive(on: RunLoop.main)
// .assign(to: \.countryResponse, on: self)
// .store(in: &disposables)
}
public func getAllCountries(){
loginHandler.getCountryList()
}
public func getAllUnis(_ id:Int){
loginHandler.getCountryList()
}
}
View Code
struct ContentViewCollection: View {
#Binding var selectedCountryId:Int
var axis : Axis.Set = .horizontal
var viewModel:CountryViewModel
#State var countries:[CountryResponse] = []
var body: some View {
ScrollView (.horizontal, showsIndicators: false) {
HStack {
ForEach(self.viewModel.countryResponse) { (postData) in
Button(action: {
self.selectedCountryId = postData.countryID ?? 0
}){
WebImage(url: self.imageURL(postData.countryName ?? ""))
.resizable()
.indicator(.activity)
.scaledToFit()
.frame(minWidth: 40,maxWidth: 40, minHeight: 40, maxHeight: 40, alignment: .center)
}
}
}
}.onAppear() {
self.loadData()
}
}
}
Thanks in advance.
I'm playing around a tad with Combine and SwiftUI for a little pet project of mine, learning as I go.
Here's the LoginModel at it current state:
public class LoginModel: ObservableObject {
#Published var domain: String = ""
#Published var email: String = ""
#Published var password: String = ""
#Published var isValid: Bool = false
public var didChange = PassthroughSubject<Void, Never>()
var credentialsValidPublisher: AnyPublisher<Bool, Never> {
Publishers.CombineLatest($email, $password)
.receive(on: RunLoop.main)
.map { (email, password) in
let emailValid = String.emailValid(emailString: email) // String extension function
let passwordValid = password.count > 5
return emailValid && passwordValid
}
.breakpointOnError()
.eraseToAnyPublisher()
}
init() {
// This works just fine
_ = credentialsValidPublisher.sink { isValid in
self.isValid = isValid
}
// However this does not work at all
_ = domain
.publisher
.receive(on: RunLoop.main)
.sink { value in
print(value)
}
}
}
Now from my current understanding is that a #Published var foo: String already has a Publisher attached to it. And that one should be able to use this directly to subscribe to its changes.
Changing credentialsValidPublisher variable over to this works too:
var credentialsValidPublisher: AnyPublisher<Bool, Never> {
Publishers.CombineLatest3($domain, $email, $password)
.receive(on: RunLoop.main)
.map { (domain, email, password) in
let domainValid = URL.isValidURL(urlString: domain)
let emailValid = String.emailValid(emailString: email)
let passwordValid = password.count > 5
return domainValid && emailValid && passwordValid
}
.breakpointOnError()
.eraseToAnyPublisher()
}
But this is not what I want. In my case I need a special Publisher in order to map a valid URL string over to a network request and then ping the server at hand to see if the provided server is responding.
Also this model is then connected to a SwiftUI view with a bunch of SwiftUI TextFields.
Any help to point me in the right direction will be highly appreciated.
So I figure out the way to do it.
In the LoginModel underneath var credentialsValidPublisher: AnyPublisher<Bool, Never> I added:
var domainValidPublisher: AnyPublisher<Bool, Never> {
$domain
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.removeDuplicates()
.map { domain in
URL.isValidURL(urlString: domain)
}
.eraseToAnyPublisher()
}
Then I can just subscribe to it in init.
I also added AnyCancellable properties that we call .cancel() on deinit.
Here's what the updated model looks like:
public class LoginModel: ObservableObject {
#Published var domain: String = ""
#Published var email: String = ""
#Published var password: String = ""
#Published var isValid: Bool = false
public var didChange = PassthroughSubject<Void, Never>()
private var credentialsValidPublisherCancellable: AnyCancellable!
private var domainValidCancellable: AnyCancellable!
var credentialsValidPublisher: AnyPublisher<Bool, Never> {
Publishers.CombineLatest3($domain, $email, $password)
.receive(on: RunLoop.main)
.map { (domain, email, password) in
let domainValid = URL.isValidURL(urlString: domain)
let emailValid = String.emailValid(emailString: email)
let passwordValid = password.count > 5
return domainValid && emailValid && passwordValid
}
.breakpointOnError()
.eraseToAnyPublisher()
}
var domainValidPublisher: AnyPublisher<Bool, Never> {
$domain
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.removeDuplicates()
.map { domain in
URL.isValidURL(urlString: domain)
}
.eraseToAnyPublisher()
}
init() {
credentialsValidPublisherCancellable = credentialsValidPublisher.sink { isValid in
self.isValid = isValid
}
domainValidCancellable = domainValidPublisher.sink { isValid in
print("isValid: \(isValid)")
}
}
deinit {
credentialsValidPublisherCancellable.cancel()
domainValidCancellable.cancel()
}
}
I am trying to replicate the "Wizard School Signup"-example which was given in the WWDC 2019 session "Combine in Practice" https://developer.apple.com/videos/play/wwdc2019/721/ starting at 22:50 using SwiftUI (as opposed to UIKit, which was used during the session).
I have created all the publishers from the example: validatedEMail, validatedPassword and validatedCredentials. While validatedEMail and validatedPassword work just fine, validatedCredentials, which consumes both publishers using CombineLatest, never fires
//
// RegistrationView.swift
//
// Created by Lars Sonchocky-Helldorf on 04.07.19.
// Copyright © 2019 Lars Sonchocky-Helldorf. All rights reserved.
//
import SwiftUI
import Combine
struct RegistrationView : View {
#ObjectBinding var registrationModel = RegistrationModel()
#State private var showAlert = false
#State private var alertTitle: String = ""
#State private var alertMessage: String = ""
#State private var registrationButtonDisabled = true
#State private var validatedEMail: String = ""
#State private var validatedPassword: String = ""
var body: some View {
Form {
Section {
TextField("Enter your EMail", text: $registrationModel.eMail)
SecureField("Enter a Password", text: $registrationModel.password)
SecureField("Enter the Password again", text: $registrationModel.passwordRepeat)
Button(action: registrationButtonAction) {
Text("Create Account")
}
.disabled($registrationButtonDisabled.value)
.presentation($showAlert) {
Alert(title: Text("\(alertTitle)"), message: Text("\(alertMessage)"))
}
.onReceive(self.registrationModel.validatedCredentials) { newValidatedCredentials in
self.registrationButtonDisabled = (newValidatedCredentials == nil)
}
}
Section {
Text("Validated EMail: \(validatedEMail)")
.onReceive(self.registrationModel.validatedEMail) { newValidatedEMail in
self.validatedEMail = newValidatedEMail != nil ? newValidatedEMail! : "EMail invalid"
}
Text("Validated Password: \(validatedPassword)")
.onReceive(self.registrationModel.validatedPassword) { newValidatedPassword in
self.validatedPassword = newValidatedPassword != nil ? newValidatedPassword! : "Passwords to short or don't matchst"
}
}
}
.navigationBarTitle(Text("Sign Up"))
}
func registrationButtonAction() {
let trimmedEMail: String = self.registrationModel.eMail.trimmingCharacters(in: .whitespaces)
if (trimmedEMail != "" && self.registrationModel.password != "") {
NetworkManager.sharedInstance.registerUser(NetworkManager.RegisterRequest(uid: trimmedEMail, password: self.registrationModel.password)) { (status) in
if status == 200 {
self.showAlert = true
self.alertTitle = NSLocalizedString("Registration successful", comment: "")
self.alertMessage = NSLocalizedString("please verify your email and login", comment: "")
} else if status == 400 {
self.showAlert = true
self.alertTitle = NSLocalizedString("Registration Error", comment: "")
self.alertMessage = NSLocalizedString("already registered", comment: "")
} else {
self.showAlert = true
self.alertTitle = NSLocalizedString("Registration Error", comment: "")
self.alertMessage = NSLocalizedString("network or app error", comment: "")
}
}
} else {
self.showAlert = true
self.alertTitle = NSLocalizedString("Registration Error", comment: "")
self.alertMessage = NSLocalizedString("username / password empty", comment: "")
}
}
}
class RegistrationModel : BindableObject {
#Published var eMail: String = ""
#Published var password: String = ""
#Published var passwordRepeat: String = ""
public var didChange = PassthroughSubject<Void, Never>()
var validatedEMail: AnyPublisher<String?, Never> {
return $eMail
.debounce(for: 0.5, scheduler: RunLoop.main)
.removeDuplicates()
.flatMap { username in
return Future { promise in
self.usernameAvailable(username) { available in
promise(.success(available ? username : nil))
}
}
}
.eraseToAnyPublisher()
}
var validatedPassword: AnyPublisher<String?, Never> {
return Publishers.CombineLatest($password, $passwordRepeat)
.debounce(for: 0.5, scheduler: RunLoop.main)
.map { password, passwordRepeat in
guard password == passwordRepeat, password.count > 5 else { return nil }
return password
}
.eraseToAnyPublisher()
}
var validatedCredentials: AnyPublisher<(String, String)?, Never> {
return Publishers.CombineLatest(validatedEMail, validatedPassword)
.map { validatedEMail, validatedPassword in
guard let eMail = validatedEMail, let password = validatedPassword else { return nil }
return (eMail, password)
}
.eraseToAnyPublisher()
}
func usernameAvailable(_ username: String, completion: (Bool) -> Void) {
let isValidEMailAddress: Bool = NSPredicate(format:"SELF MATCHES %#", "[A-Z0-9a-z._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}").evaluate(with: username)
completion(isValidEMailAddress)
}
}
#if DEBUG
struct RegistrationView_Previews : PreviewProvider {
static var previews: some View {
RegistrationView()
}
}
#endif
I expected the form button to get enabled when a valid username (valid E-Mail-address) and two matching passwords with the right length are provided. The two Publishers responsible for those two tasks work, I can see the validatedEMail and the validatedPassword in the user interface in the two Texts which I added for debugging purposes.
Just the third Publisher (also compare to the code shown in the Video from above at 32:20) never fires. I did set breakpoints in those Publishers, in the validatedPassword Publisher at line:
guard password == passwordRepeat, password.count > 5 else { return nil }
which stopped there just fine but a similar breakpoint in the validatedCredentials Publisher at line:
guard let eMail = validatedEMail, let password = validatedPassword else { return nil }
was never reached.
What did I do wrong?
Edit:
In order to make the above code run under Xcode-beta 11.0 beta 4 didChange needs to be replaced with willChange
I've got this question answered here: https://forums.swift.org/t/crash-in-swiftui-app-using-combine-was-using-published-in-conjunction-with-state-in-swiftui/26628/9 by the very friendly and helpful Nanu Jogi, who is not on stackoverflow.
It is rather straight forward:
add this line:
.receive(on: RunLoop.main) // run on main thread
in validatedCredentials so that it looks like this:
var validatedCredentials: AnyPublisher<(String, String)?, Never> {
return Publishers.CombineLatest(validatedEMail, validatedPassword)
.receive(on: RunLoop.main) // <<—— run on main thread
.map { validatedEMail, validatedPassword in
print("validatedEMail: \(validatedEMail ?? "not set"), validatedPassword: \(validatedPassword ?? "not set")")
guard let eMail = validatedEMail, let password = validatedPassword else { return nil }
return (eMail, password)
}
.eraseToAnyPublisher()
This is all what is needed.
And here one more time the whole code for reference (updated for Xcode 11.0 beta 5 (11M382q)):
//
// RegistrationView.swift
// Combine-Beta-Feedback
//
// Created by Lars Sonchocky-Helldorf on 09.07.19.
// Copyright © 2019 Lars Sonchocky-Helldorf. All rights reserved.
//
import SwiftUI
import Combine
struct RegistrationView : View {
#ObservedObject var registrationModel = RegistrationModel()
#State private var registrationButtonDisabled = true
#State private var validatedEMail: String = ""
#State private var validatedPassword: String = ""
var body: some View {
Form {
Section {
TextField("Enter your EMail", text: $registrationModel.eMail)
SecureField("Enter a Password", text: $registrationModel.password)
SecureField("Enter the Password again", text: $registrationModel.passwordRepeat)
Button(action: registrationButtonAction) {
Text("Create Account")
}
.disabled($registrationButtonDisabled.wrappedValue)
.onReceive(self.registrationModel.validatedCredentials) { newValidatedCredentials in
self.registrationButtonDisabled = (newValidatedCredentials == nil)
}
}
Section {
Text("Validated EMail: \(validatedEMail)")
.onReceive(self.registrationModel.validatedEMail) { newValidatedEMail in
self.validatedEMail = newValidatedEMail != nil ? newValidatedEMail! : "EMail invalid"
}
Text("Validated Password: \(validatedPassword)")
.onReceive(self.registrationModel.validatedPassword) { newValidatedPassword in
self.validatedPassword = newValidatedPassword != nil ? newValidatedPassword! : "Passwords to short or don't match"
}
}
}
.navigationBarTitle(Text("Sign Up"))
}
func registrationButtonAction() {
}
}
class RegistrationModel : ObservableObject {
#Published var eMail: String = ""
#Published var password: String = ""
#Published var passwordRepeat: String = ""
var validatedEMail: AnyPublisher<String?, Never> {
return $eMail
.debounce(for: 0.5, scheduler: RunLoop.main)
.removeDuplicates()
.map { username in
return Future { promise in
print("username: \(username)")
self.usernameAvailable(username) { available in
promise(.success(available ? username : nil))
}
}
}
.switchToLatest()
.eraseToAnyPublisher()
}
var validatedPassword: AnyPublisher<String?, Never> {
return Publishers.CombineLatest($password, $passwordRepeat)
.debounce(for: 0.5, scheduler: RunLoop.main)
.map { password, passwordRepeat in
print("password: \(password), passwordRepeat: \(passwordRepeat)")
guard password == passwordRepeat, password.count > 5 else { return nil }
return password
}
.eraseToAnyPublisher()
}
var validatedCredentials: AnyPublisher<(String, String)?, Never> {
return Publishers.CombineLatest(validatedEMail, validatedPassword)
.receive(on: RunLoop.main)
.map { validatedEMail, validatedPassword in
print("validatedEMail: \(validatedEMail ?? "not set"), validatedPassword: \(validatedPassword ?? "not set")")
guard let eMail = validatedEMail, let password = validatedPassword else { return nil }
return (eMail, password)
}
.eraseToAnyPublisher()
}
func usernameAvailable(_ username: String, completion: (Bool) -> Void) {
let isValidEMailAddress: Bool = NSPredicate(format:"SELF MATCHES %#", "[A-Z0-9a-z._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}").evaluate(with: username)
completion(isValidEMailAddress)
}
}
#if DEBUG
struct RegistrationView_Previews : PreviewProvider {
static var previews: some View {
RegistrationView()
}
}
#endif
Just replace
.debounce(for: 0.5, scheduler: RunLoop.main)
with
.throttle(for: 0.5, scheduler: RunLoop.main, latest: true)
Since there is no expensive code in the publishers subscription no deferred processing would be basically needed. Throttling the key events with latest: true will do the job almost in the same way.
I'm not such a Reactive programming expert that I can judge what is the reason behind, I assume a design choice.
You might need to group some of these publisher's validation into one consumer. There is a cool playground outlining the combine framework and this is how they do a similar use case. In the example they are validating the user name and password within the same subscriber. The subscriber does not execute until something has been published to the user name and password publishers.
If you wanted to keep them separate then you would need to add some more publishers that basically outline the state of whether the password is valid and the user name is valid. Then have subscribers listening to when both the user name and password publishers are valid.
I'm new to RxSwift and trying implement app that using MVVM architecture. I have view model:
class CategoriesViewModel {
fileprivate let api: APIService
fileprivate let database: DatabaseService
let categories: Results<Category>
// Input
let actionRequest = PublishSubject<Void>()
// Output
let changeset: Observable<(AnyRealmCollection<Category>, RealmChangeset?)>
let apiSuccess: Observable<Void>
let apiFailure: Observable<Error>
init(api: APIService, database: DatabaseService) {
self.api = api
self.database = database
categories = database.realm.objects(Category.self).sorted(byKeyPath: Category.KeyPath.name)
changeset = Observable.changeset(from: categories)
let requestResult = actionRequest
.flatMapLatest { [weak api] _ -> Observable<Event<[Category]>> in
guard let strongAPI = api else {
return Observable.empty()
}
let request = APIService.MappableRequest(Category.self, resource: .categories)
return strongAPI.mappedArrayObservable(from: request).materialize()
}
.shareReplayLatestWhileConnected()
apiSuccess = requestResult
.map { $0.element }
.filterNil()
.flatMapLatest { [weak database] newObjects -> Observable<Void> in
guard let strongDatabase = database else {
return Observable.empty()
}
return strongDatabase.updateObservable(with: newObjects)
}
apiFailure = requestResult
.map { $0.error }
.filterNil()
}
}
and I have following binginds in view controller:
viewModel.apiSuccess
.map { _ in false }
.bind(to: refreshControl.rx.isRefreshing)
.disposed(by: disposeBag)
viewModel.apiFailure
.map { _ in false }
.bind(to: refreshControl.rx.isRefreshing)
.disposed(by: disposeBag)
But if I comment bindings, part with database updating stops executing. I need to make it execute anyway, without using dispose bag in the view model. Is it possible?
And little additional question: should I use weak-strong dance with api/database and return Observable.empty() like in my view model code or can I just use unowned api/unowned database safely?
Thanks.
UPD:
Function for return observable in APIService:
func mappedArrayObservable<T>(from request: MappableRequest<T>) -> Observable<[T]> {
let jsonArray = SessionManager.jsonArrayObservable(with: request.urlRequest, isSecured: request.isSecured)
return jsonArray.mapResponse(on: mappingSheduler, { Mapper<T>().mapArray(JSONArray: $0) })
}
Work doesn't get done unless there is a subscriber prepared to receive the results.
Your DatabaseService needs to have a dispose bag in it and subscribe to the Observable<[Category]>. Something like:
class ProductionDatabase: DatabaseService {
var categoriesUpdated: Observable<Void> { return _categories }
func updateObservable(with categories: Observable<[Category]>) {
categories
.subscribe(onNext: { [weak self] categories in
// store categories an then
self?._categories.onNext()
})
.disposed(by: bag)
}
private let _categories = PublishSubject<Void>()
private let bag = DisposeBag()
}
Then apiSuccess = database.categoriesUpdated and database.updateObservable(with: requestResult.map { $0.element }.filterNil())