I want to check text validate when tap button - rx-swift

I want to check text validate when tap button. If validate failure, it will show error text on UILabel and do not send request. Else if validate success, it will send request.
I see many demo about login, but they control button enabled to avoid validate data when tap button. I am puzzled with it.
I have write some code.
class LoginViewModel: BaseViewModel, ViewModelType {
struct Input {
let loginTaps: Driver<Void>
}
struct Output {
let validatedUsername: Driver<Bool>
let validatedPassword: Driver<Bool>
}
let username = BehaviorRelay(value: "")
let password = BehaviorRelay(value: "")
let loginTapped = PublishSubject<Void>()
func transform(input: Input) -> Output {
let validatedUsername = username.asDriver(onErrorJustReturn: "").map { username in
return username.isPhoneNumber
}
let validatedPassword = password.asDriver(onErrorJustReturn: "").map { password in
return password.count > 7
}
input.loginTaps.map { () -> Void in
<#code#>
// I want do check and then do network request
}
input.loginTaps.drive(onNext: { [weak self] () in
self?.loginTapped.onNext(())
}).disposed(by: rx.disposeBag)
loginTapped.flatMapLatest { _ -> Observable<RxSwift.Event<Token>> in
// and if I want to return Bool not Token, how should I do????????????
return self.provider.login(username: self.username.value, password: self.password.value)
.asObservable()
.materialize()
}.subscribe(onNext: { (event) in
switch event {
case .next(let token):
AuthManager.setToken(token: token)
case .error(let error):
log.error(error.localizedDescription)
default: break
}
}).disposed(by: rx.disposeBag)
return Output(validatedUsername: validatedUsername,
validatedPassword: validatedPassword)
}
}

I would expect to see something like this:
class LoginViewModel {
struct Input {
let loginTap: Signal<Void>
let username: Driver<String>
let password: Driver<String>
}
struct Output {
let errorText: Driver<String>
let loginSuccess: Signal<Void>
}
var networkRequest: (URLRequest) -> Observable<Data> = { _ in fatalError("need to replace this with an implementation.") }
func transform(input: Input) -> Output {
func isValidCredentials(username: String, password: String) -> Bool {
return username.isPhoneNumber && password.count > 7
}
let credentials = Driver.combineLatest(input.username, input.password)
// this chain emits when the login button is tapped and the credentials are invalid
let invalidInputError = input.loginTap
.withLatestFrom(credentials)
.filter { !isValidCredentials(username: $0, password: $1) }
.map { _ in "Credentials are invalid" }
.asDriver(onErrorRecover: { _ in fatalError("can't get here") })
let networkRequest = self.networkRequest // to avoid dealing with `self` in the flatMap below
// this chain makes a request if the login button is tapped while the credentials are valid
let loginResult = input.loginTap
.withLatestFrom(credentials)
.filter { isValidCredentials(username: $0, password: $1) }
.map { URLRequest.login(username: $0, password: $1) }
.asObservable()
.flatMapLatest { networkRequest($0).materialize() }
.share(replay: 1)
// this chain emits when the login result produces an error
let loginError = loginResult
.map { $0.error }
.filter { $0 != nil }
.map { $0!.localizedDescription }
.asDriver(onErrorRecover: { _ in fatalError("can't get here") })
// this chain emits when the login result succeeds
let loginSuccess = loginResult
.filter { $0.element != nil }
.map { _ in }
.asSignal(onErrorRecover: { _ in fatalError("can't get here") })
let errorText = Driver.merge(invalidInputError, loginError)
return Output(errorText: errorText, loginSuccess: loginSuccess)
}
}

Related

SwiftUI & WidgetKit: Why Intent Handler does not load saved data?

I'm building a Widget with dynamic configuration. I provide a dynamic list of options with an Intents Extension - inside Intent Handler (code below).
However only sampleData shows up, but not the userScrums when tapping on "Edit Widget".
Why it doesn't load userScrums?
import Intents
class IntentHandler: INExtension {
override func handler(for intent: INIntent) -> Any {
// This is the default implementation. If you want different objects to handle different intents,
// you can override this and return the handler you want for that particular intent.
return self
}
}
extension IntentHandler: ScrumSelectionIntentHandling {
func provideScrumOptionsCollection(for intent: ScrumSelectionIntent) async throws -> INObjectCollection<ScrumType> {
let sampleScrums = DailyScrum.sampleData.map { scrum in
ScrumType(identifier: scrum.title, display: scrum.title)
}
let userScrums = try? await ScrumStore.load().map { scrum in
ScrumType(identifier: scrum.title, display: scrum.title)
}
let allScrums = sampleScrums + (userScrums ?? [])
let collection = INObjectCollection(items: allScrums)
return collection
}
// func provideScrumOptionsCollection(for intent: ScrumSelectionIntent, with completion: #escaping (INObjectCollection<ScrumType>?, Error?) -> Void) {}
}
import Foundation
import SwiftUI
class ScrumStore: ObservableObject {
#Published var scrums: [DailyScrum] = []
private static func fileURL() throws -> URL {
try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appendingPathComponent("scrums.data")
}
static func load() async throws -> [DailyScrum] {
try await withCheckedThrowingContinuation { continuation in
load { result in
switch result {
case .failure(let error):
continuation.resume(throwing: error)
case .success(let scrums):
continuation.resume(returning: scrums)
}
}
}
}
static func load(completion: #escaping (Result<[DailyScrum], Error>)->Void) {
DispatchQueue.global(qos: .background).async {
do {
let fileURL = try fileURL()
guard let file = try? FileHandle(forReadingFrom: fileURL) else {
DispatchQueue.main.async {
completion(.success([]))
}
return
}
let dailyScrums = try JSONDecoder().decode([DailyScrum].self, from: file.availableData)
DispatchQueue.main.async {
completion(.success(dailyScrums))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
#discardableResult
static func save(scrums: [DailyScrum]) async throws -> Int {
try await withCheckedThrowingContinuation { continuation in
save(scrums: scrums) { result in
switch result {
case .failure(let error):
continuation.resume(throwing: error)
case .success(let scrumsSaved):
continuation.resume(returning: scrumsSaved)
}
}
}
}
static func save(scrums: [DailyScrum], completion: #escaping (Result<Int, Error>)->Void) {
DispatchQueue.global(qos: .background).async {
do {
let data = try JSONEncoder().encode(scrums)
let outfile = try fileURL()
try data.write(to: outfile)
DispatchQueue.main.async {
completion(.success(scrums.count))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
}

How can I use a list of names out of CoreData with a Picker in menuPickerStyle

I'm looking for a way to read a list of players from CoreData in order to select a single name with a menupickerstyle-picker. Displaying the entire list works fine.
this works fine:
Text("Number of players: (teams.count) ")
How con a display a single name?
Text("Players Name: (???????????) ")
Thx for any help.
Franz
//DataBase: teamsandscores Entity: Team, Attributes: id as UUID and player as String
class CoreDataManager {
let persistentContainer: NSPersistentContainer
init() {
persistentContainer = NSPersistentContainer(name: "teamsandscores")
persistentContainer.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Core Data Store failed \(error.localizedDescription)")
}
}
}
func updateTeam() {
do {
try persistentContainer.viewContext.save()
} catch{
persistentContainer.viewContext.rollback()
}
}
func deleteTeam(team: Team){
persistentContainer.viewContext.delete(team)
do {
try persistentContainer.viewContext.save()
} catch{
persistentContainer.viewContext.rollback()
print("Failed to save context \(error)")
}
}
func getAllTeams() -> [Team] {
let fetchRequest: NSFetchRequest<Team> = Team.fetchRequest()
do{
return try persistentContainer.viewContext.fetch(fetchRequest)
}
catch {
return []
}
}
func saveTeams(player: String){
let teamCD = Team(context: persistentContainer.viewContext)
teamCD.player = player
do {
try persistentContainer.viewContext.save()
} catch {
print("failed to save team name \(error)")
}
}
}
let coreDM: CoreDataManager
#State private var playerHome : String = ""
#State private var allPlayersCIndex = 1
#State private var teams: [Team] = [Team]()
private func populateTeams(){
teams = coreDM.getAllTeams()
}
var body: some View{
VStack{
HStack{
TextField("NewPlayer", text: $playerHome )
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Save") {
coreDM.saveTeams(player: playerHome)
populateTeams()
playerHome = ""
}
}
List{
ForEach(teams, id: \.self) { name in
Text(name.player ?? "")
}
.onDelete(perform: { indexSet in
indexSet.forEach { index in
let team = teams[index]
coreDM.deleteTeam(team: team)
populateTeams()
}
})
}
///works fine:
Text("Number of players: \(teams.count) ")
///// I need help.
Text("Players Name: \(???????????[0]) ")
}
HStack{
///? Picker("Player", selection: $allPlayersCIndex,
content: {
ForEach(teams, id: \.self) { name in
///?? Text(name.player ?? "")
}
}
)
}

RXSwift why loading variable return wrong value?

Here is my view model:
let loadMoreTrigger = PublishSubject<Void>()
let refreshTrigger = PublishSubject<Void>()
let loading = BehaviorRelay<Bool>(value: false)
let stories = BehaviorRelay<[Story]>(value: [])
var offset = 0
let error = PublishSubject<String>()
let selectedFeedType: BehaviorRelay<FeedType> = BehaviorRelay(value: .best)
override init() {
super.init()
let refreshRequest = loading.asObservable().sample(refreshTrigger).flatMap { loading -> Observable<[Story]> in
if loading {
return Observable.empty()
} else {
self.offset = 0
return self.fetchStories(type: self.selectedFeedType.value, offset: self.offset)
}
}
let loadMoreRequest = loading.asObservable().sample(loadMoreTrigger).flatMap { loading -> Observable<[Story]> in
if loading {
return Observable.empty()
} else {
self.offset += 10
return self.fetchStories(type: self.selectedFeedType.value, offset: self.offset)
}
}
let request = Observable.merge(refreshRequest, loadMoreRequest).share(replay: 1)
let response = request.flatMap { (stories) -> Observable<[Story]> in
request.do(onError: { error in
self.error.onNext(error.localizedDescription)
}).catchError { (error) -> Observable<[Story]> in
Observable.empty()
}
}.share(replay: 1)
Observable.combineLatest(request, response, stories.asObservable()) { request, response, stories in
return self.offset == 0 ? response : stories + response
}.sample(response).bind(to: stories).disposed(by: disposeBag)
Observable.merge(request.map{_ in true}, response.map{_ in false}, error.map{_ in false}).bind(to: loading).disposed(by: disposeBag)
}
Then when i checking loading observer i have false -> true, instead of true -> false. I just don't understand why it happening.
loading.subscribe {
print($0)
}.disposed(by: disposeBag)
In my viewController i call refreshTrigger on viewWillAppear using rx.sentMessage
Here is getFeed function:
func getFeed(type: FeedType, offset: Int) -> Observable<[Story]> {
return provider.rx.request(.getFeed(type: type, offset: offset)).asObservable().flatMap { (response) -> Observable<[Story]> in
do {
let feedResponse = try self.jsonDecoder.decode(BaseAPIResponse<[Story]>.self, from: response.data)
guard let stories = feedResponse.data else { return .error(APIError.requestFailed)}
return .just(stories)
} catch {
return .error(error)
}
}.catchError { (error) -> Observable<[Story]> in
return .error(error)
}
}
Your request and response observables are emitting values at the exact same time. Which one shows up in your subscribe first is undefined.
Specifically, request doesn't emit a value until after the fetch request completes. Try this instead:
Observable.merge(
loadMoreTrigger.map { true },
refreshTrigger.map { true },
response.map { _ in false },
error.map { _ in false }
)
.bind(to: loading)
.disposed(by: disposeBag)
There are lots of other problems in your code but the above answers this specific question.

Update SwiftUI List after network request using RxSwift

I need to update a SwiftUI List after making a network request. For requests, I use Moya approach with combination of triggers(Input&Output - "Kickstarter").
I cant use Combine framework due to the structure of the project, while they have a lot of helpful advises(not sure about my case).
Simple ContactList:
struct ContactList: View {
var viewModel: UserViewModel
var body: some View {
NavigationView {
List(viewModel.users) { contact in
NavigationLink(destination: ContactDetail(user: contact)) {
ContactRow(user: contact)
}
}
.navigationBarTitle(Text("Team Members"))
}
}
}
Then ViewModel
class UserViewModel {
let disposeBag = DisposeBag()
var users: [TeamMember] = []
init(users: [TeamMember] = []) {
let networkModel = UserNetworkModel()
networkModel.output.teamMembers.subscribe { (event) in
self.users.append(contentsOf: event.element.orEmpty)
}.disposed(by: disposeBag)
networkModel.output.error.subscribe(onNext: { error in
print(error.localizedDescription)
}).disposed(by: disposeBag)
networkModel.input.loadTrigger.onNext(Void())
self.users = users
}
}
And NetworkModel
class UserNetworkModel {
let disposeBag = DisposeBag()
let input: Input
let output: Output
struct Input {
let loadTrigger: AnyObserver<Void>
let searchTrigger: AnyObserver<String>
}
struct Output {
let teamMembers: Observable<[TeamMember]>
let error: Observable<Error>
}
internal let loadSubject = PublishSubject<Void>()
internal let searchSubject = BehaviorSubject<String>(value: "")
internal let errorSubject = PublishSubject<Error>()
internal let teamMembersSubject = BehaviorSubject<[TeamMember]>(value: [])
init() {
let service = MoyaProvider<TeamTarget>()
self.input = Input(loadTrigger: loadSubject.asObserver(), searchTrigger: searchSubject.asObserver())
self.output = Output(teamMembers: teamMembersSubject.asObservable(), error: errorSubject.asObservable())
let result = loadSubject.flatMapLatest { _ -> Observable<[TeamMember]> in
service.rx.request(.get).debug().mapArray(TeamMember.self).asObservable()
}.share(replay: 1)
Observable.combineLatest(result, searchSubject).map { (arg) in
let (members, filter) = arg
if filter.isEmpty {
return members
} else {
let searchText = try! self.searchSubject.value()
return members.filter({
return [$0.firstName, $0.lastName]
.compactMap({ $0 })
.first(where: { $0.hasPrefix(searchText) }) != nil
})
}
}.bind(to: teamMembersSubject).disposed(by: disposeBag)
result.subscribe(onError: { error in
self.errorSubject.onNext(error)
}).disposed(by: self.disposeBag)
}
}
Is it possible to update users array in this way? Or only Combine can do it for me easily?
Thanks for your time.
While I've found some Combine solution. Still waiting for Rx solution.
So, I fixed it just added #ObservedObject for ContactList property and #Published for users' array in Network model. Much less code, natively but not what I was looking for.
Full answer:
struct ContactList: View {
#ObservedObject var networkModel: NetworkModel
var body: some View {
NavigationView {
List(networkModel.users) { contact in
NavigationLink(destination: ContactDetail(user: contact)) {
ContactRow(user: contact)
}
}
.navigationBarTitle(Text("Team Members"))
}
}
}
#if DEBUG
struct ContactList_Previews: PreviewProvider {
static var previews: some View {
ContactList(networkModel: .init(users: contactData))
}
}
#endif
class NetworkModel: ObservableObject {
#Published var users = [User]()
init(users: [User] = []) {
getMembers()
}
private func getMembers() {
let provider = MoyaProvider<TeamTarget>()
provider.request(.get) { [weak self] (result) in
switch result {
case .success(let response):
do {
let result = try JSONDecoder().decode([User].self, from: response.data)
self?.users.append(contentsOf: result)
} catch {
print(error.localizedDescription)
}
case .failure(let error):
print(error.errorDescription.orEmpty)
}
}
}
}

Swift Alamofire + Promise catching

Folks, The following works except for the catch, xcode errors out with expected member name following '.'
Is this the proper way to promisify with PromiseKit?
All suggestions welcome! Thanks!
#IBAction func loginButtonTapped(sender: AnyObject) {
let email = userEmail.text!
let password = userPassword.text!
func onSuccess(success:Bool, message:String, token: String) -> Promise<Void> {
if success {
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "isUserLoggedIn")
NSUserDefaults.standardUserDefaults().synchronize()
self.dismissViewControllerAnimated(true, completion: nil)
} else {
let myAlert = UIAlertController(title: "Alert", message: message, preferredStyle: UIAlertControllerStyle.Alert)
let okAction = UIAlertAction(title: "Try Again", style: UIAlertActionStyle.Default, handler: nil)
myAlert.addAction(okAction)
self.presentViewController(myAlert, animated: true, completion: nil)
}
return Promise { resolve, reject in
return resolve()
}
}
func onFailure(error:NSError) -> Promise<Void> {
return Promise { resolve, reject in
return reject(error)
}
}
Login(email, password: password).then(onSuccess).catch(onFailure)
}
private func Login(email: String, password: String) -> Promise<(success:Bool, message:String, token: String)> {
let parameters: [String: String] = [
"username" : email,
"password" : password
];
let endpoint = "https://api.foo.bar/login"
return Promise { resolve, reject in
Alamofire.request(.POST, endpoint, parameters: parameters, encoding: .JSON)
.validate()
.responseJSON { (response) in
guard response.result.error == nil else {
logger.debug(response)
let result:(success:Bool, message:String, token: String) = (false, "Login Failed", "")
return resolve(result)
}
if let value = response.result.value {
let apiResponseJSONBody = JSON(value)
let result:(success:Bool, message:String, token: String) = (true, "", token: apiResponseJSONBody["token"].string!)
return resolve(result)
}
}
}
}

Resources