Triggering CombineLatest to propagate initial value in Combine - swift

I've got to two String publishers and one computed property which returns AnyPublisher. Logic is quite simple but I would like to know if there is any way to propagate initial value. I think it should be somehow possible since publishers have initial values.
In VC I'm assigning new values to Publishers from ViewModel (from textField).
firstTextField.addTarget(self, action: #selector(firstTextFieldDidChange(_:)), for: .editingChanged)
secondTextField.addTarget(self, action: #selector(secondTextFieldDidChange(_:)), for: .editingChanged)
#objc private func firstTextFieldDidChange(_ textField: UITextField) {
viewModel.firstPublisher = textField.text ?? ""
}
#objc private func secondTextFieldDidChange(_ textField: UITextField) {
viewModel.secondPublisher = textField.text ?? ""
}
And then I'm assigning Publisher (combineLatest) to my button:
_ = viewModel.validatedText
.receive(on: RunLoop.main)
.assign(to: \.isEnabled, on: button)
In VM I've got two Publishers:
#Published var firstPublisher: String = ""
#Published var secondPublisher: String = ""
and CombineLatest:
var validatedText: AnyPublisher<Bool, Never> {
return Publishers.CombineLatest($firstPublisher, $secondPublisher) {
return !($0.isEmpty || $1.isEmpty)
}.eraseToAnyPublisher()
}
validatedText only starts publishing new values when I start typing in both text fields. I tried assigning some new values in init of VM for example (to first and second Publisher) but it also didn't work. Is there any way to do it or I will have to set initial state of button (disable it) without using combine?

Unfortunately, it seems like this just may be the behavior of #Published, but you can work around this in your generated Publisher by prepending an initial value:
var validatedText: AnyPublisher<Bool, Never> {
let validate: (String, String) -> Bool = {
!($0.isEmpty || $1.isEmpty)
}
return Publishers.CombineLatest($firstPublisher, $secondPublisher, transform: validate)
.prepend(validate(firstPublisher, secondPublisher))
.eraseToAnyPublisher()
}
Conversely, it is fairly trivial to write your own property delegate to get the behavior you want if you'd rather take that approach:
import Combine
#propertyDelegate
struct InitialPublished<Value> : Publisher {
typealias Output = Value
typealias Failure = Never
private let subject: CurrentValueSubject<Output, Failure>
var value: Value {
set { subject.value = newValue }
get { subject.value }
}
init(initialValue: Value) {
subject = CurrentValueSubject(initialValue)
}
func receive<S>(subscriber: S) where S: Subscriber, Value == S.Input, Failure == S.Failure {
subject.receive(subscriber: subscriber)
}
}

Related

RxSwift, RxCocoa - no called when writing TextField after validation

I am new to RxSwift and RxCocoa
I need to any advice for learning
After result of Checking Id Validation, expect no word in label
But it is updating label and no entering in break point at bind function
What’s problem my code…?
var disposeBag: DisposeBag = DisposeBag()
let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
let input: Signal<String> = userIDTextField.rx.text.orEmpty
.asSignal(onErrorSignalWith: .empty())
let output: Driver<String> = viewModel.bind(input)
disposeBag.insert(
output.drive(userIDLabel.rx.text)
)
}
struct ViewModel {
func checkUserIDFromDB(id: String) -> Signal<Bool> {
return .just(false).asSignal()
}
func bind(_ input: Signal<String>) -> Driver<String> {
let validState = input
.map { _ in self.checkUserIDFromDB(id:)}
.withLatestFrom(input)
return validState.asDriver(onErrorDriveWith: .empty())
}
}
This line: .map { _ in self.checkUserIDFromDB(id:)} produces a Signal<(String) -> Signal<Bool>> which is likely not what you wanted.
I'm going to assume that the goal here is to pass the entered string to the network request and wait for it to emit. If it emits true then emit the string to the label, otherwise do nothing...
Further, let's simplify things by using the Observable type instead of Signals and Drivers:
final class ViewController: UIViewController {
let userIDTextField = UITextField()
let userIDLabel = UILabel()
let disposeBag = DisposeBag() // this should be a `let` not a `var`
let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
let input = userIDTextField.rx.text.orEmpty
let output = viewModel.bind(input.asObservable())
disposeBag.insert(
output.bind(to: userIDLabel.rx.text)
)
}
}
struct ViewModel {
func checkUserIDFromDB(id: String) -> Observable<Bool> { .just(false) }
func bind(_ input: Observable<String>) -> Observable<String> {
input.flatMapLatest { id in // Note this should be `flatMapLatest` not `map`
Observable.zip( // zip up the text with its response
Observable.just(id),
self.checkUserIDFromDB(id: id) // you weren't actually making the network call. This makes it.
.catchAndReturn(false) // if the call fails, emit `false`.
)
}
.compactMap { $0.1 ? $0.0 : nil } // if the response is true, emit the text, else nothing
}
}
The biggest concern I have with this code is what happens if the user continues to type. This will fire after every character the user enters which could be a lot of network requests, the flatMapLatest will cancel ongoing requests that are no longer needed, but still... Consider putting a debounce in the stream to reduce the number of requests.
Learn more about the various versions of flatMap from this article.
Edit
In response to your comment. In my opinion, a ViewModel should not be dependent on RxCocoa, only RxSwift. However, if you feel you must use Driver, then something like this would be appropriate:
func bind(_ input: ControlProperty<String>) -> Driver<String> {
input.asDriver()
.flatMapLatest { id in
Driver.zip(
Driver.just(id),
self.checkUserIDFromDB(id: id)
.asDriver(onErrorJustReturn: false)
)
}
.compactMap { $0.1 ? $0.0 : nil }
}
Using Signal doesn't make much sense in this context.

Using Combine to parse phone number String

I'm trying to wrap my mind around how Combine works. I believe I'm doing something wrong when I use the .assign operator to mutate the #Published property I'm operating on. I've read the documentation on Publishers, Subscribers, and Operators. But I'm a bit loose on where exactly to create the Publisher if I don't want it to be a function call.
import SwiftUI
import Combine
struct PhoneNumberField: View {
let title: String
#ObservedObject var viewModel = ViewModel()
var body: some View {
TextField(title,text: $viewModel.text)
}
class ViewModel: ObservableObject {
#Published var text: String = ""
private var disposables = Set<AnyCancellable>()
init() {
$text.map { value -> String in
self.formattedNumber(number: value)
}
//something wrong here
.assign(to: \.text, on: self)
.store(in: &disposables)
}
func formattedNumber(number: String) -> String {
let cleanPhoneNumber = number.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
let mask = "+X (XXX) XXX-XXXX"
var result = ""
var index = cleanPhoneNumber.startIndex
for ch in mask where index < cleanPhoneNumber.endIndex {
if ch == "X" {
result.append(cleanPhoneNumber[index])
index = cleanPhoneNumber.index(after: index)
} else {
result.append(ch)
}
}
return result
}
}
}
struct PhoneNumberParser_Previews: PreviewProvider {
static var previews: some View {
PhoneNumberField(title: "Phone Number")
}
}
Use .receive(on:):
$text.map { self.formattedNumber(number: $0) }
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] value in
self?.text = value
})
.store(in: &disposables)
This will allow you to listen to changes of the text variable and update it in the main queue. Using main queue is necessary if you want to update #Published variables read by some View.
And to avoid having a retain cycle (self -> disposables -> assign -> self) use sink with a weak self.

Combine #Published property send values also when not updated

I was trying to create a dynamic Form using SwiftUI and Combine, that loads options of an input (in the example, number) based on another input (in the example, myString).
The problem is that the Combine stack get executed continuously, making lots of network requests (in the example, simulated by the delay), even if the value is never changed.
I think that the expected behavior is that $myString publishes values only when it changes.
class MyModel: ObservableObject {
// My first choice on the form
#Published var myString: String = "Jhon"
// My choice that depends on myString
#Published var number: Int?
var updatedImagesPublisher: AnyPublisher<Int, Never> {
return $myString
.removeDuplicates()
.print()
.flatMap { newImageType in
return Future<Int, Never> { promise in
print("Executing...")
// Simulate network request
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
let newNumber = Int.random(in: 1...200)
return promise(.success(newNumber))
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
struct ContentView: View {
#ObservedObject var model: MyModel = MyModel()
var body: some View {
Text("\(model.number ?? -100)")
.onReceive(model.updatedImagesPublisher) { newNumber in
self.model.number = newNumber
}
}
}
The problem is the updatedImagesPublisher is a computed property. It means that you create a new instance every time you access it. What happens in your code. The Text object subscribes to updatedImagesPublisher, when it receives a new value, it updates the number property of the Model. number is #Published property, it means that objectWillChange method will be called every time you change it and the body will be recreated. New Text will subscribe to new updatedImagesPublisher (because it is computed property) and receive the value again. To avoid such behaviour just use lazy property instead of computed property.
lazy var updatedImagesPublisher: AnyPublisher<Int, Never> = {
return $myString
.removeDuplicates()
.print()
.flatMap { newImageType in
return Future<Int, Never> { promise in
print("Executing...")
// Simulate network request
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
let newNumber = Int.random(in: 1...200)
return promise(.success(newNumber))
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}()
I assume it is because you create new publisher for every view update, try the following instead. (Tested with Xcode 11.4)
class MyModel: ObservableObject {
// My first choice on the form
#Published var myString: String = "Jhon"
// My choice that depends on myString
#Published var number: Int?
lazy var updatedImagesPublisher: AnyPublisher<Int, Never> = {
return $myString
.removeDuplicates()
.print()
.flatMap { newImageType in
return Future<Int, Never> { promise in
print("Executing...")
// Simulate network request
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
let newNumber = Int.random(in: 1...200)
return promise(.success(newNumber))
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}()
}

Swift slow performance when filtering array

I have an array of a Category class that has a name and parentName. I have a search bar to allow users search for categories by the category name or parentName. My full array has about 600 items. On typing the first letter it takes about 2-3 seconds and freezes all other input on the keyboard. After the first letter everything is fast.
Here is how I am filtering
return self.userData.categories.filter({$0.name.lowercased().hasPrefix(searchText.lowercased()) || ($0.parentName != nil && $0.parentName!.lowercased().hasPrefix(searchText.lowercased()))})
One piece I think it may be is SwiftUI rendering all of the rows, however the initial render is fast.
This is how I render the categories.
List(categories) { category in
CategoryPickerRowView(category: category, isSelected: category.id == self.transaction.categoryId)
.onTapGesture { self.transaction.categoryId = category.id }
}
Update:
I noticed when the first letter is typed or deleted (when it is slow) I get this message in the logs
[Snapshotting] Snapshotting a view (0x7fb6bd4b8080, _UIReplicantView) that has not been rendered at least once requires afterScreenUpdates:YES.
You can use Combine to debounce your search so it happens only after the user stops typing. You can also use Combine to move your filter to the background and then move your assign back to the main queue.
class Model: ObservableObject {
#Published var searchResults: [String] = []
let searchTermSubject = CurrentValueSubject<String, Never>("")
let categorySubject = CurrentValueSubject<String, Never>("")
private var subscriptions = Set<AnyCancellable>()
init() {
Publishers
.CombineLatest(
searchTermSubject
.debounce(for: .milliseconds(250), scheduler: RunLoop.main),
categorySubject
)
.receive(on: DispatchQueue.global(qos: .userInteractive))
.map { combined -> [String] in
// Do search here
}
.receive(on: RunLoop.main)
.assign(to: \.searchResults, on: self)
.store(in: &subscriptions)
}
}
By adding .id(UUID()) to the list it fixes the problem.
List(categories) { category in
CategoryPickerRowView(category: category, isSelected: category.id == self.transaction.categoryId)
.onTapGesture { self.transaction.categoryId = category.id }
}.id(UUID())
Description found here: https://www.hackingwithswift.com/articles/210/how-to-fix-slow-list-updates-in-swiftui
Use textfield delegate : -
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
{
if string.isEmpty
{
search = String(search.dropLast())
}else{
search=textField.text!+string
}
and use ".contain" to search name.
eg: -
var search:String = ""
var searchData:NSArray?
let filtered = rewardArray?.filter { ($0.name .lowercased()).contains(search.lowercased()) }
searchData = filtered as NSArray?
and reload your collection or table with searchData

Boolean returns nil and unable to access value from response on view controller

I have a usermodel that checks the backend if the email exists - then I drill back into a viewcontroller and set a boolean value that should trigger a function run. However the value is unchanged and I am trying to change this value from the usermodel but it is not accessible. I understand why it does not work.. but do not know how to resolve the issue.
static func sendEmailWithResetLink(email: String) {
let params : Parameters = [
PARAM_EMAIL : email
]
request(URL_RESET_PASSWORD as String, method: .post, parameters: params, headers: nil).responseJSON {
(response: DataResponse<Any>) in
hideProgress()
print("this is response \(response)")
switch(response.result)
{
case .success(_):
print("it did not fail")
let passwordResetVC = PasswordResetViewController()
passwordResetVC.hasFailed = false
break
case .failure(_):
print("it failed")
let passwordResetVC = PasswordResetViewController()
//here boolean is set that I am trying to access in viewcontroller
passwordResetVC.hasFailed = true
break
}
}
}
Here's what I would suggest. You probably have some of these in place already:
Create an PasswordResetViewController object has an #IBAction func resetButtonClicked triggered by a button or whatever, which kicks off the password reset process.
Create a UserManager class. This class is responsible for all profile management activies in your app. Among other things, it has the ability to reset user passwords. This UserManager would probably be a singleton, that' sprobably good enough for now.
Create a new UserManagerDelegate protocol. Add to it all capabilities that are required by the UserManager to inform them of whatever happened. For example: var passwordResetHasFailed: Bool { get set }.
Extend your PasswordResetViewController conform to this protocol.
Your VC gets a reference to the singleton UserManager object, stores it in an instance variable, and uses that to access the shared object from then on.
Make your PasswordResetViewController register itself as the delegate to the user manager, with userManager.delegate = self
The #IBAction func resetButtonClicked will just call userManager.resetPassword()
Your UserManager does whatever it needs to do to reset the user's password.
When it's done, it'll call self.delegate?.passwordResetHasFailed = true/false.
Since your PasswordResetViewController registered itself as the delegate of the UserManager, when the operation is done, its passwordResetHasFailed property will be changed, giving it a chance to respond (by updating some UI or whatever).
There are some limitations to this approach, but it's a decent way to get started. Some thing to note:
This lets you unit test your PasswordResetViewController. You can create a MockUserManager, and set tesPasswordResetViewController.userManager = MockUserManager(), allowing you to separate out the user manager, and test PasswordResetViewController in isolation.
You'll run into issues if you need multiple objects to subscribe to receive delegate call backs (since there can only be 1 delegate object). At that point, you can switch to using something like Promises, RxSwift or Combine. But that's a problem for a later time, and the migration would be easy.
Going off of #Alexander - Reinstate Monica and what I assume what the code to look like to approach your problem.
Using MVC:
In Models folder (data/ logic part)
public class User {
private var name: String!
private var userEmail: String!
public var hasFailed: Bool?
init() {
name = ""
userEmail = ""
hasFailed = nil
}
public func setName(name: String) { self.name = name }
public func getName() -> String { return name }
public func setEmail(email: String) { userEmail = email }
public func getEmail() ->String { return userEmail }
public static func sendEmailWithRestLing(email: String) {
// your other code
switch response.result {
case .success(_):
//your code
hasFailed = false
break
case .failuare(_):
// your code
hasFailed = true
break
}
}
}
User Manager class applying singleton design
final class UserManager {
private var user = User()
static let instance = UserManager()
private init(){}
public func userName(name: String) {
if (name.count > 3) {
user.setName(name: name)
}
else { print("user name is too short") }
}
public func userEmail(email: String) {
if (email.count > 3) {
user.setEmail(email: email)
}
else { print("user email is too short") }
}
public func getUserName() -> String {
let name = user.getName()
if (name.isEmpty) { return "user name is Empty" }
return name
}
public func getUserEmail() -> String {
let email = user.getEmail()
if (email.isEmpty) { return "user email is Empty" }
return email
}
public func doKatieTask(link: String) -> Int {
guard let myValue = user.hasFailed else {
return -1
}
if (myValue) { return 1}
return 0
}
}
So, Now in the Controllers folder and since we a one-to-one relation we will use delegate design pattern. If had had one-to-many with the view controller. Use observers.
class ViewController: UIViewController {
#IBOutlet weak var nameTextField: UITextField!
#IBOutlet weak var emailTextField: UITextField!
var _hasFail: Bool!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func doTask() {
UserManager.instance.userName(name: nameTextField.text!)
UserManager.instance.userEmail(email: emailTextField.text!)
switch UserManager.instance.doKatieTask(link: emailTextField.text!) {
case 0:
_hasFail = false
break
case 1:
_hasFail = true
break
default:
print("hasFailed is nil")
break
}
if let vc = storyboard?.instantiateViewController(identifier: "passwordVC") as? PasswordResetViewController {
vc.modalPresentationStyle = .fullScreen
vc.delegate = self
self.present(vc, animated: true, completion: nil)
}
}
}
extension ViewController: KatieDelegate {
var hasFailed: Bool {
get {
return _hasFail
}
set {
_hasFail = newValue
}
}
}
In PasswordReset UIViewController
protocol KatieDelegate {
var hasFailed: Bool { get set }
}
class PasswordResetViewController: UIViewController {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var emailLabel: UILabel!
var delegate: KatieDelegate?
override func viewDidLoad() {
super.viewDidLoad()
nameLabel.text = UserManger.instance.getUserName()
emailLabel.text = UserManger.instance.getUserEmail()
if let delegate = delegate {
print("The value for has failed is: .....\(delegate.hasFailed)!")
}
else { print("error with delegate") }
}
}