I'm new to RxSwift and want to achieve the following. I have a email and password TextField. When you've entered a text in both textfields a button should be enabled.
In my ViewController I do the following:
txtEmail.rx.text.asObservable()
.bindTo(viewModel.email)
.addDisposableTo(disposeBag)
txtPassword.rx.text.asObservable()
.bindTo(viewModel.password)
.addDisposableTo(disposeBag)
viewModel.buttonEnabled
.bindTo(btnLogin.rx.isEnabled)
.addDisposableTo(disposeBag)
And here is my ViewModel:
import Foundation
import RxSwift
import RxCocoa
class LoginViewModel {
let email = Variable<String?>("")
let password = Variable<String?>("")
var buttonEnabled: Observable<Bool>
init() {
var processedEmail: Observable<String>!
var processedPassword: Observable<String>!
processedEmail = email.asObservable().map(String.toLower as! (String?) -> String).map(String.trimWhiteSpace as! (String?) -> String)
processedPassword = password.asObservable().map(String.toLower as! (String?) -> String).map(String.trimWhiteSpace as! (String?) -> String)
let emailValid = processedEmail.asObservable().map(String.isNotEmpty)
let passwordValid = processedPassword.asObservable().map(String.isNotEmpty)
buttonEnabled = Observable.combineLatest(emailValid, passwordValid) {
return $0 && $1
}
}
func didTapLoginButton() {
print("hello \(email.value)")
}
}
For some reason the init method of my viewmodel never gets finished.
Can someone help me?
It seems like the definition of processedEmail and processedPassword are generating the crash preventing the view model init to complete.
More specifically, the String.toLower and String.trimWhiteSpace method probably have type (String) -> String but here they are force cast to (String?) -> String.
processedEmail = email.asObservable()
.map { $0 ?? "" }
.map(String.toLower)
.map(String.trimWhiteSpace)
processedPassword = password.asObservable()
.map { $0 ?? "" }
.map(String.toLower)
.map(String.trimWhiteSpace)
I've only added .map { $0 ?? "" }. Because the original observable has type Observable<String?> the map call will transform it to Observable<String> which will then be processable by String.toLower and String.trimWhiteSpace
I tried this example of init and it seems working:
init() {
let processedEmail = email.asObservable()
.map { $0 ?? "" }
.map { $0.lowercased() }
.map { !$0.isEmpty }
let processedPassword = password.asObservable()
.map { $0 ?? "" }
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
.map { !$0.isEmpty }
buttonEnabled = Observable.combineLatest(processedEmail, processedPassword) {
return $0 && $1
}
}
Related
I have one function which is having some logic which have 2 foreach loop but i want to make code compact so I am trying to use compactmap
func getData() -> [String] {
var ids = [String]()
self.item?.connections?.forEach { connection in
connection.validLine?.forEach { line in
if let _ = line.connection?.links[LinkKey.dataGroups],
let dataGroups = line.dataGroupsCache, dataGroups.isContinue {
ids += checkinGroups.connections?.compactMap { $0.id } ?? []
}
}
}
return ids
}
so instead of 2 foreach i am trying to make in one by using self.item?.connections?.compactMap({ $0.validline }) but I am getting error saying "Type of expression is ambiguous without more context"
I don't see how you can do it without to forEach or compactMap. Here is a possible solution:
func getData() -> [String] {
return item?.connections?.compactMap { connection in
connection.validLine?.compactMap { line in
guard let _ = line.connection?.links[LinkKey.dataGroups], line.dataGroupsCache?.isContinue == true else { return nil }
return checkinGroups.connections?.compactMap(\.id)
}
}
}
Here's a translation of your post into something that is compilable and a direct translation into a version that doesn't use forEach.
I changed connectionIds to ids in your example because otherwise, you might as well just return [].
class Example {
func getData() -> [String] {
var ids = [String]()
self.item?.connections?.forEach { connection in
connection.validLine?.forEach { line in
if let _ = line.connection?.links[LinkKey.dataGroups],
let dataGroups = line.dataGroupsCache, dataGroups.isContinue {
ids += checkinGroups.connections?.compactMap { $0.id } ?? []
}
}
}
return ids
}
func getDataʹ() -> [String] {
guard let connections = item?.connections else { return [] }
let numberOfProperLines = connections.flatMap { $0.validLine ?? [] }
.filter { line in
if let _ = line.connection?.links[LinkKey.dataGroups],
let dataGroups = line.dataGroupsCache, dataGroups.isContinue {
return true
} else {
return false
}
}
.count
return (0..<numberOfProperLines).flatMap { _ in checkinGroups.connections?.compactMap(\.id) ?? [] }
}
var checkinGroups: CheckInGroups!
var item: Item!
}
enum LinkKey: Int {
case dataGroups
}
struct Item {
let connections: [Connection]?
}
struct Connection {
let id: String?
let validLine: [Line]?
let links: [LinkKey: Void]
}
struct Line {
let dataGroupsCache: DataGroups?
let connection: Connection?
}
struct DataGroups {
let isContinue: Bool
}
struct CheckInGroups {
let connections: [Connection]?
}
I have an application where I want to make an API call once the screen is awaken in the ViewController. Basically, I am using Universal Link to activate the ViewCOntroller and when it displays the UIViewController, I want to make an API call based on the Data got. I am currently using the MVVM Architecture and I have added my code below
My ViewModel
class EmailVerificationViewModel: ViewModel, ViewModelType {
struct Input {
let editEmailTrigger: Driver<Void>
}
struct Output {
}
let routeManager: BehaviorRelay<RouteMatchResult?>
let currentEmail: BehaviorRelay<String?>
init(routeManager: RouteMatchResult?, provider: Api, currentEmail: String?) {
self.routeManager = BehaviorRelay(value: routeManager)
self.currentEmail = BehaviorRelay(value: currentEmail)
super.init(provider: provider)
}
func transform(input: Input) -> Output {
// THE CALL I WANT TO MAKE
routeManager.errorOnNil().asObservable()
.flatMapLatest { (code) -> Observable<RxSwift.Event<User>> in
log("=========++++++++++++==========")
// guard let code = code else {return}
let params = code.values
let challengeId = Int(params["xxx"] as? String ?? "0")
let login = LoginResponseModel(identifier: params["xxxx"] as? String, key: params["xxxxxx"] as? String, oth: params["xxxxx"] as? String, id: 0, challengeId: challengeId)
return self.provider.postVerifyApp(challengeId: login.challengeId!, oth: login.oth!, identifier: login.identifier!)
.trackActivity(self.loading)
.trackError(self.error)
.materialize()
}.subscribe(onNext: { [weak self] (event) in
switch event {
case .next(let token):
log(token)
AuthManager.setToken(token: token)
// self?.tokenSaved.onNext(())
case .error(let error):
log(error.localizedDescription)
default: break
}
}).disposed(by: rx.disposeBag)
return Output()
}
}
My Viewcontroller
override func bindViewModel() {
super.bindViewModel()
guard let viewModel = viewModel as? EmailVerificationViewModel else { return }
let input = EmailVerificationViewModel.Input(editEmailTrigger: editEmailBtn.rx.tap.asDriver())
let output = viewModel.transform(input: input)
viewModel.loading.asObservable().bind(to: isLoading).disposed(by: rx.disposeBag)
viewModel.parsedError.asObservable().bind(to: error).disposed(by: rx.disposeBag)
isLoading.asDriver().drive(onNext: { [weak self] (isLoading) in
isLoading ? self?.startAnimating() : self?.stopAnimating()
}).disposed(by: rx.disposeBag)
error.subscribe(onNext: { [weak self] (error) in
var title = ""
var description = ""
let image = R.image.icon_toast_warning()
switch error {
case .serverError(let response):
title = response.message ?? ""
}
self?.view.makeToast(description, title: title, image: image)
}).disposed(by: rx.disposeBag)
}
so how can I make the call on the commented like THE CALL I WANT TO MAKE once the application catches the universal link and loads up. Basically making an API call on viewDidLoad
The code in your sample was way more than is needed to answer the question. Here is how you make a network call on viewDidLoad:
class ViewController: UIViewController {
var viewModel: ViewModel!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let input = ViewModel.Input()
let output = viewModel.transform(input: input)
output.viewData
.bind(onNext: { viewData in
// setup the view with viewData
})
.disposed(by: disposeBag)
}
}
class ViewModel {
struct Input { }
struct Output {
let viewData: Observable<ViewData>
}
init(api: API) {
self.api = api
}
func transform(input: Input) -> Output {
let viewData = api.networkCall()
.map { ViewData(from: $0) }
return Output(viewData: viewData)
}
let api: API
}
In my view controller I have a UISearchController associated with the UITableView. So all my normal table view datasource methods do the old
if isSearching {
// use filteredTableData array
} else {
// use SharedModel.shared.participants
}
I'm not clear on how I'd implement that using RxCocoa as I'm brand new to Rx.
Create a Variable like below
var tableViewOptions = Variable<[String]>([]) // replace String with your own object
bind tableViewOptions to tableview after your view loads.
tableViewOptions
.asObservable()
.bind(to: self.tableView
.rx
.items(cellIdentifier: "cellIdentifier",
cellType: CustomCell.self)) { _, values, cell in
// do your stuff
}
Then when ever you search change the values of tableViewOptions like below.
if isSearching {
tableViewOptions.value = filteredTableArray
} else {
tableViewOptions.value = SharedModel.shared.participants
}
I solve this by declaring a decorator for an Observable and an extension for a UISearchBar (you could also declare it for the UISearchController):
//FIXME: Item can't be type constrained. Have to use optional casting.
class FilterableByTitleCollection<Item>: ObservableType {
private let origin: Observable<Array<Item>>
private let filteringStrategySource: Observable<TitlableModelFilteringStrategy> //FIXME: This is a strategy source
init<Origin: ObservableType>(
origin: Origin,
filteringStrategySource: Observable<TitlableModelFilteringStrategy>) where Origin.E == Array<Item> {
self.origin = origin.asObservable()
self.filteringStrategySource = filteringStrategySource
}
typealias E = Array<Item>
func subscribe<O:ObserverType>(_ observer: O) -> Disposable where O.E == Array<Item> {
return Observable.combineLatest(
origin,
filteringStrategySource
)
.observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
.map{ origin, strategy in
guard origin is Array<Titlable> else { assert(false); return origin }
return origin.filter{ strategy.shouldInclude(item: $0 as! Titlable) }
}
.observeOn(MainScheduler.instance)
.subscribe(observer)
}
}
...
extension UISearchBar {
var titlableFilteringStrategy: Observable<TitlableModelFilteringStrategy> {
return Observable<String?>.merge(
[
self.rx.text.asObservable(),
self.rx.textDidEndEditing
.map{ [weak self] in
assert(self != nil)
return self?.text
},
self.rx.cancelButtonClicked.map{ Optional<String>.some("") }
]
).distinctUntilChanged{ (old: String?, new: String?) -> Bool in
old == new
}.map{ TitlableModelFilteringStrategy(filteringPredicate: $0) }
}
}
...
struct TitlableModelFilteringStrategy {
private let filteringPredicate: String
init(filteringPredicate: String?) {
self.filteringPredicate = filteringPredicate ?? ""
}
func shouldInclude(item: Titlable) -> Bool {
return filteringPredicate.isEmpty ? true : item.title.localizedCaseInsensitiveContains(filteringPredicate)
}
func equals(to another: TitlableModelFilteringStrategy) -> Bool {
return filteringPredicate == another.filteringPredicate
}
}
...
protocol Titlable {
var title: String { get }
}
I'm writing a DataService that interfaces with Firebase. I'm setting self.realID within a closure and when I reference it outside the closure, it fails because it unwraps a nil value. Why is this happening?
My file:
import Foundation
import Firebase
class Database {
var firebaseRef = Firebase(url:"https://<<UNIQUE>>.firebaseio.com")
class var sharedInstance: Database {
struct Data {
static var instance: Database?
static var token: dispatch_once_t = 0
}
dispatch_once(&Data.token) {
Data.instance = Database()
}
return Data.instance!
}
var uid : String!
var realID : String!
var validated = false
func validate(user: String, study: String) -> Bool {
firebaseRef.authUser(user+"#example.com", password: user,
withCompletionBlock: { error, authData in
if error != nil {
NSLog(String(error))
} else {
self.uid = authData.uid
NSLog(authData.uid)
}
})
let usersRef = Firebase(url: "https://<<UNIQUE>>.firebaseio.com/users")
usersRef.observeEventType(FEventType.Value, withBlock: { (snapshot) in
let value = snapshot.value.objectForKey("study") as! String
self.realID = value
NSLog(self.realID) // this is a non-nil value
})
NSLog("About to encounter nil value and crash")
if self.realID == study {
return true
}
return false
}
}
How do i prevent this fatal error from happening?
You need to add a completionHandler because it is async request. If you will set the break points then return is executed before you are setting the id.
func validate(user: String, study: String, completionHandler:(Bool) -> Void) {
let usersRef = Firebase(url: "https://<<UNIQUE>>.firebaseio.com/users")
usersRef.observeEventType(FEventType.Value, withBlock: { (snapshot) in
if let value = snapshot.value.objectForKey("study") as? String {
self.realID = value
completionHandler(true)
} else {
completionHandler(false)
}
})
}
UPDATE
validate("Rahul", study: "Study") { (value: Bool) in
if value {
} else {
}
}
I have a syncing engine with a server that follows this code for created updated and deleted objects
let lastSynchronizationDate = (NSUserDefaults.standardUserDefaults().objectForKey("com.fridge.sync") as [String: NSDate])["skin"]!
query.whereKey("updatedAt", greaterThanOrEqualTo: lastSynchronizationDate)
query.findObjectsInBackgroundWithBlock { (remoteSkins, error) -> Void in
if error != nil {
return
}
if remoteSkins.isEmpty {
return
}
RLMRealm.defaultRealm().transactionWithBlock {
let deletedSkins = (remoteSkins as [PFObject]).filter {
$0["state"] as Int == 0
}.map {
Skin(forPrimaryKey: $0["color"])
}.filter {
$0 != nil
}.map {
$0!
}
let createdSkins = (remoteSkins as [PFObject]).filter {
$0["state"] as Int != 0
}.filter {
let skin = Skin(forPrimaryKey: $0["color"])
println(skin.name)
return skin == nil
}.map { (remoteSkin) -> Skin in
let remoteSkinModel = RemoteSkinModel(remoteSkin)
let skin = Skin()
skin.skinModel = remoteSkinModel // Error
return skin
}
let updatedSkins = (remoteSkins as [PFObject]).filter {
$0["state"] as Int != 0
}.map {
($0, Skin(forPrimaryKey: $0["color"]))
}.filter {
$0.1 != nil
}.map {
$0.1.skinModel = RemoteSkinModel($0.0)
}
RLMRealm.defaultRealm().deleteObjects(deletedSkins)
RLMRealm.defaultRealm().addObjects(createdSkins)
}
}
My skin model:
class Skin: RLMObject {
dynamic var name: String = ""
dynamic var hexString: String = ""
var skinModel: SkinModel! {
didSet {
name = skinModel.name ?? oldValue.name ?? ""
hexString = skinModel.hexString ?? oldValue.name ?? ""
}
}
override class func primaryKey() -> String {
return "hexString"
}
override class func ignoredProperties() -> [AnyObject] {
return ["skinModel"]
}
func save() {
}
}
protocol SkinModel {
var name: String { get }
var hexString: String { get }
}
class RemoteSkinModel: SkinModel {
let remoteSkin: PFObject
let name: String
let hexString: String
init(_ remoteSkin: PFObject) {
self.remoteSkin = remoteSkin
self.name = remoteSkin["name"] as? String ?? ""
self.hexString = remoteSkin["color"] as? String ?? ""
}
}
The first the engine runs everything goes well, but the next time it pops the bad access error when comparing if the realm object is nil or not in the createdSkins' code.
Any idea as to why this would happen?
Thanks in advance
What happens if you force skin to be an optional?
let skin: Sin? = Skin(forPrimaryKey: $0["color"])
if let skin = skin {
println(skin.name)
}
return skin == nil
I think the Swift compiler may be preventing checks to nil because Skin(forPrimaryKey:) should be a failable initializer, but Realm can't mark that as such (only Apple code can have those).
If that's not the issue, let me know and we'll keep troubleshooting this together. (I work at Realm).