Reset TextField value using Combine and Swiftui - ios

I try to reset a TextField value when a certain condition is met (.count == 4), but it does not work, what am I missing?
class ViewModel: ObservableObject {
#Published var code = ""
private var anyCancellable: AnyCancellable?
init() {
anyCancellable = $code.sink { (newVal) in
if newVal.count == 4 {
self.code = ""
}
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
TextField("My code", text: $viewModel.code)
}
}

This is a case where you don't need any Combine. Just use the normal didSet to observe changes to the property:
class ViewModel: ObservableObject {
#Published var code = "" {
didSet {
if code.count == 4 {
self.code = ""
}
}
}
}

Adding .receive(on: DispatchQueue.main) seems to fix this issue, however, I am not entirly sure why it is needed.
On a side note, make sure you capture [weak self] in a sink block to avoid memory leaks:
anyCancellable = $code
.receive(on: DispatchQueue.main) // <--
.sink { [weak self] newVal in
if newVal.count == 4 {
self?.code = ""
}

Related

How to update TextField value after debounce

Suppose I am using the following code.
class TextFieldObserver : ObservableObject {
#Published var realText = ""
#Published var searchText = ""
private var subscriptions = Set<AnyCancellable>()
init(tester: Binding<String>) {
$searchText
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
.sink(receiveValue: { t in
self.realText = t
} )
.store(in: &subscriptions)
}
func updateDebounced() {
realText = "this is a test"
}
}
struct MainView: View {
#ObseredObject var observer: TextFieldObserver
var body: some View {
VStack {
TextField("Search text", $observer.searchText)
Button("Update debounced text", action: {observer.updateDebounced()})
}
}
}
I want to know how I can update the variable observer.searchText when calling observer.updateDebounced() (i.e. I want to update searchText after updating realText since realText represents the text stored in the model and searchText is just an intermediate value).

SwiftUI var: willSet, didSet

I have this kind of code in a SwiftUI app:
#Published var myFlag = false {
willSet {
... useful code ...
objectWillChange.send()
}
}
It more or less works, but to exactly get what I need I want to have something like that:
#Published var myFlag = false {
didSet {
... useful code ...
objectDidChange.send()
}
}
For the simple reason that I would like to be notified once myFlag is updated rather than before. It doesn't seem to work that way though.
Is there some other way to get what I want?
Beside I am using this code:
.onReceive(self.appState.$myFlag) {...}
inside ContentView.swift
#Published automatically publishes when the property is at willSet
https://developer.apple.com/documentation/combine/published
If you prefer to publish during didSet remove #Published and publish manually.
import SwiftUI
class ManualOObject: ObservableObject {
var someValue: Int = 0{
willSet{
print("willSet - \(someValue)")
}
didSet{
print("didSet - \(someValue)")
objectWillChange.send()
}
}
init() {
for n in 0...5 {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(n)) {
print("n = \(n.description)")
self.someValue = n
}
}
}
}
struct ManualOOView: View {
#StateObject var manualOObject = ManualOObject()
var body: some View {
Text(manualOObject.someValue.description).onReceive(manualOObject.objectWillChange, perform: { _ in
print("UI received = \(manualOObject.someValue)")
})
}
}

SwiftUI: How can I catch changing value from observed object when I execute function

I have a problem with observed object in SwiftUI.
I can see changing values of observed object on the View struct.
However in class or function, even if I change text value of TextField(which is observable object) but "self.codeTwo.text still did not have changed.
here's my code sample (this is my ObservableObject)
class settingCodeTwo: ObservableObject {
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: settingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: settingCodeTwo.userDefaultTextKey)
}
}
deinit {
canc.cancel()
}
}
and the main problem is... "self.codeTwo.text" never changed!
class NetworkManager: ObservableObject {
#ObservedObject var codeTwo = settingCodeTwo()
#Published var posts = [Post]()
func fetchData() {
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "\(self.codeTwo.text)" //This one I want to use observable object
urlComponents.path = "/mob_json/mob_json.aspx"
urlComponents.queryItems = [
URLQueryItem(name: "nm_sp", value: "UP_MOB_CHECK_LOGIN"),
URLQueryItem(name: "param", value: "1000|1000|\(Gpass.hahaha)")
]
if let url = urlComponents.url {
print(url)
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.Table
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
and this is view, I can catch change of the value in this one
import SwiftUI
import Combine
struct SettingView: View {
#ObservedObject var codeTwo = settingCodeTwo()
var body: some View {
ZStack {
Rectangle().foregroundColor(Color.white).edgesIgnoringSafeArea(.all).background(Color.white)
VStack {
TextField("test", text: $codeTwo.text).textFieldStyle(BottomLineTextFieldStyle()).foregroundColor(.blue)
Text(codeTwo.text)
}
}
}
}
Help me please.
Non-SwiftUI Code
Use ObservedObject only for SwiftUI, your function / other non-SwiftUI code will not react to the changes.
Use a subscriber like Sink to observe changes to any publisher. (Every #Published variable has a publisher as a wrapped value, you can use it by prefixing with $ sign.
Reason for SwiftUI View not reacting to class property changes:
struct is a value type so when any of it's properties change then the value of the struct has changed
class is a reference type, when any of it's properties change, the underlying class instance is still the same.
If you assign a new class instance then you will notice that the view reacts to the change.
Approach:
Use a separate view and that accepts codeTwoText as #Binding that way when the codeTwoText changes the view would update to reflect the new value.
You can keep the model as a class so no changes there.
Example
class Model : ObservableObject {
#Published var name : String //Ensure the property is `Published`.
init(name: String) {
self.name = name
}
}
struct NameView : View {
#Binding var name : String
var body: some View {
return Text(name)
}
}
struct ContentView: View {
#ObservedObject var model : Model
var body: some View {
VStack {
Text("Hello, World!")
NameView(name: $model.name) //Passing the Binding to name
}
}
}
Testing
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let model = Model(name: "aaa")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
model.name = "bbb"
}
return ContentView(model: model)
}
}
It is used two different instances of SettingCodeTwo - one in NetworkNamager another in SettingsView, so they are not synchronised if created at same time.
Here is an approach to keep those two instances self-synchronised (it is possible because they use same storage - UserDefaults)
Tested with Xcode 11.4 / iOS 13.4
Modified code below (see also important comments inline)
extension UserDefaults {
#objc dynamic var textKey2: String { // helper keypath
return string(forKey: "textKey2") ?? ""
}
}
class SettingCodeTwo: ObservableObject { // use capitalised name for class !!!
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: SettingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
private var observer: NSKeyValueObservation!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: SettingCodeTwo.userDefaultTextKey)
}
observer = UserDefaults.standard.observe(\.textKey2, options: [.new]) { _, value in
if let newValue = value.newValue, self.text != newValue { // << avoid cycling on changed self
self.text = newValue
}
}
}
}
class NetworkManager: ObservableObject {
var codeTwo = SettingCodeTwo() // no #ObservedObject needed here
...

Binding a SwiftUI Button to AnySubscriber like RxCocoa's button tap

I use the following UIViewController and RxSwift/RxCocoa based piece of code to write a very simply MVVM pattern to bind a UIButton tap event to trigger some Observable work and listen for the result:
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
#IBOutlet weak var someButton: UIButton!
var viewModel: ViewModel!
private var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel = ViewModel()
setupBindings()
}
private func setupBindings() {
someButton.rx.tap
.bind(to: self.viewModel.input.trigger)
.disposed(by: disposeBag)
viewModel.output.result
.subscribe(onNext: { element in
print("element is \(element)")
}).disposed(by: disposeBag)
}
}
class ViewModel {
struct Input {
let trigger: AnyObserver<Void>
}
struct Output {
let result: Observable<String>
}
let input: Input
let output: Output
private let triggerSubject = PublishSubject<Void>()
init() {
self.input = Input(trigger: triggerSubject.asObserver())
let resultObservable = triggerSubject.flatMap { Observable.just("TEST") }
self.output = Output(result: resultObservable)
}
}
It compiles and runs well. However, I need to Combinify this pattern with SwiftUI, so I converted that code into the following:
import SwiftUI
import Combine
struct ContentView: View {
var viewModel: ViewModel
var subscriptions = Set<AnyCancellable>()
init(viewModel: ViewModel) {
self.viewModel = viewModel
setupBindings()
}
var body: some View {
Button(action: {
// <---- how to trigger viewModel's trigger from here
}, label: {
Text("Click Me")
})
}
private func setupBindings() {
self.viewModel.output.result.sink(receiveValue: { value in
print("value is \(value)")
})
.store(in: &subscriptions) // <--- doesn't compile due to immutability of ContentView
}
}
class ViewModel {
struct Input {
let trigger: AnySubscriber<Void, Never>
}
struct Output {
let result: AnyPublisher<String, Never>
}
let input: Input
let output: Output
private let triggerSubject = PassthroughSubject<Void, Never>()
init() {
self.input = Input(trigger: AnySubscriber(triggerSubject))
let resultPublisher = triggerSubject
.flatMap { Just("TEST") }
.eraseToAnyPublisher()
self.output = Output(result: resultPublisher)
}
}
This sample doesn't compile due to two errors (commented in code):
(1) Problem 1: How to trigger the publisher's work from the button's action closure like the case of RxSwift above ?
(2) Problem 2 is related somehow to architectural design rather than a compile error:
the error says: ... Cannot pass immutable value as inout argument: 'self' is immutable ..., that's because SwiftUI views are structs, they are designed to be changed only through sorts of bindings (#State, #ObservedObject, etc ...), I have two sub-questions related to problem 2:
[A]: is it considered a bad practice to sink a publisher in a SwiftUI View ? which may need some workaround to store the cancellable at the View's struct scope ?
[B]: which one is better for SwiftUI/Combine projects in terms of MVVM architectural pattern: using a ViewModel with [ Input[Subscribers], Output[AnyPublishers] ] pattern, or a
ObservableObject ViewModel with [ #Published properties] ?
I had same problem understanding best mvvm approach.
Recommend also look into this thread Best data-binding practice in Combine + SwiftUI?
Will post my working example. Should be easy to convert to what you want.
SwiftUI View:
struct ContentView: View {
#State private var dataPublisher: String = "ggg"
#State private var sliderValue: String = "0"
#State private var buttonOutput: String = "Empty"
let viewModel: SwiftUIViewModel
let output: SwiftUIViewModel.Output
init(viewModel: SwiftUIViewModel) {
self.viewModel = viewModel
self.output = viewModel.bind(())
}
var body: some View {
VStack {
Text(self.dataPublisher)
Text(self.sliderValue)
Slider(value: viewModel.$sliderBinding, in: 0...100, step: 1)
Button(action: {
self.viewModel.buttonBinding = ()
}, label: {
Text("Click Me")
})
Text(self.buttonOutput)
}
.onReceive(output.dataPublisher) { value in
self.dataPublisher = value
}
.onReceive(output.slider) { (value) in
self.sliderValue = "\(value)"
}
.onReceive(output.resultPublisher) { (value) in
self.buttonOutput = value
}
}
}
AbstractViewModel:
protocol ViewModelProtocol {
associatedtype Output
associatedtype Input
func bind(_ input: Input) -> Output
}
ViewModel:
final class SwiftUIViewModel: ViewModelProtocol {
struct Output {
let dataPublisher: AnyPublisher<String, Never>
let slider: AnyPublisher<Double, Never>
let resultPublisher: AnyPublisher<String, Never>
}
typealias Input = Void
#SubjectBinding var sliderBinding: Double = 0.0
#SubjectBinding var buttonBinding: Void = ()
func bind(_ input: Void) -> Output {
let dataPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
.delay(for: 5.0, scheduler: DispatchQueue.main)
.map{ "Just for testing - \($0)"}
.replaceError(with: "An error occurred")
.receive(on: DispatchQueue.main)
.share()
.eraseToAnyPublisher()
let resultPublisher = _buttonBinding.anyPublisher()
.dropFirst()
.flatMap { Just("TEST") }
.share()
.eraseToAnyPublisher()
return Output(dataPublisher: dataPublisher,
slider: _sliderBinding.anyPublisher(),
resultPublisher: resultPublisher)
}
}
SubjectBinding property wrapper:
#propertyWrapper
struct SubjectBinding<Value> {
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue: Value) {
subject = CurrentValueSubject<Value, Never>(wrappedValue)
}
func anyPublisher() -> AnyPublisher<Value, Never> {
return subject.eraseToAnyPublisher()
}
var wrappedValue: Value {
get {
return subject.value
}
set {
subject.value = newValue
}
}
var projectedValue: Binding<Value> {
return Binding<Value>(get: { () -> Value in
return self.subject.value
}) { (value) in
self.subject.value = value
}
}
}
So I recently was also wondering how I would do this since we are not starting to write out views in SwiftUI.
I made a helper object the encapsulates the transition from a function call to a Publisher. I called it a Relay.
#available(iOS 13.0, *)
struct Relay<Element> {
var call: (Element) -> Void { didCall.send }
var publisher: AnyPublisher<Element, Never> {
didCall.eraseToAnyPublisher()
}
// MARK: Private
private let didCall = PassthroughSubject<Element, Never>()
}
In your case specifically, you would be able to declare a private Relay and use it like so;
Button(action: relay.call,
label: {
Text("Click Me")
})
And then you can do whatever you like with.
relay.publisher

ObservedObject inside ObservableObject not refreshing View

I'm trying to display an activity indicator when performing an async request.
What I did is creating an ActivityTracker object that will track life cycle of a publisher.
This ActivityTracker is an ObservableObject and will be stored in the view model which also is an ObservableObject.
It seems that this kind of setup isn't refreshing the View. Here's my code:
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel()
var body: some View {
VStack(spacing: 16) {
Text("Counter: \(viewModel.tracker.count)\nPerforming: \(viewModel.tracker.isPerformingActivity ? "true" : "false")")
Button(action: {
_ = request().trackActivity(self.viewModel.tracker).sink { }
}) {
Text("Request")
}
}
}
}
class ContentViewModel: ObservableObject {
#Published var tracker = Publishers.ActivityTracker()
}
private func request() -> AnyPublisher<Void, Never> {
return Just(()).delay(for: 2.0, scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
extension Publishers {
final class ActivityTracker: ObservableObject {
// MARK: Properties
#Published var count: Int = 0
var isPerformingActivity: Bool {
return count > 0
}
private var cancellables: [AnyCancellable] = []
private let counterSubject = CurrentValueSubject<Int, Never>(0)
private let lock: NSRecursiveLock = .init()
init() {
counterSubject.removeDuplicates()
.receive(on: RunLoop.main)
.print()
.sink { [weak self] counter in
self?.count = counter
}
.store(in: &cancellables)
}
// MARK: Private methods
fileprivate func trackActivity<Value, Error: Swift.Error>(
ofPublisher publisher: AnyPublisher<Value, Error>
) {
publisher
.receive(on: RunLoop.main)
.handleEvents(
receiveSubscription: { _ in self.increment() },
receiveOutput: nil,
receiveCompletion: { _ in self.decrement() },
receiveCancel: { self.decrement() },
receiveRequest: nil
)
.print()
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })
.store(in: &cancellables)
}
private func increment() {
lock.lock()
defer { lock.unlock() }
counterSubject.value += 1
}
private func decrement() {
lock.lock()
defer { lock.unlock() }
counterSubject.value -= 1
}
}
}
extension AnyPublisher {
func trackActivity(_ activityTracker: Publishers.ActivityTracker) -> AnyPublisher {
activityTracker.trackActivity(ofPublisher: self)
return self
}
}
I also tried to declare my ActivityTracker as #Published but same result, my text is not updated.
Note that storing the activity tracker directly in the view will work but this is not what I'm looking for.
Did I miss something here ?
Nested ObservableObjects is not supported yet.
When you want to use these nested objects, you need to notify the objects by yourself when data got changed.
I hope the following code can help you with your problem.
First of all use: import Combine
Then declare your model and submodels, they all need to use the #ObservableObject property to work. (Do not forget the #Published property aswel)
I made a parent model named Model and two submodels Submodel1 & Submodel2. When you use the parent model when changing data e.x: model.submodel1.count, you need to use a notifier in order to let the View update itself.
The AnyCancellables notifies the parent model itself, in that case the View will be updated automatically.
Copy the code and use it by yourself, then try to remake your code while using this. Hope this helps, goodluck!
class Submodel1: ObservableObject {
#Published var count = 0
}
class Submodel2: ObservableObject {
#Published var count = 0
}
class Model: ObservableObject {
#Published var submodel1 = Submodel1()
#Published var submodel2 = Submodel2()
var anyCancellable: AnyCancellable? = nil
var anyCancellable2: AnyCancellable? = nil
init() {
anyCancellable = submodel1.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}
anyCancellable2 = submodel2.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}
}
}
When you want to use this Model, just use it like normal usage of the ObservedObjects.
struct Example: View {
#ObservedObject var obj: Model
var body: some View {
Button(action: {
self.obj.submodel1.count = 123
// If you've build a complex layout and it still won't work, you can always notify the modal by the following line of code:
// self.obj.objectWillChange.send()
}) {
Text("Change me")
}
}
If you have a collection of stuff you can do this:
import Foundation
import Combine
class Submodel1: ObservableObject {
#Published var count = 0
}
class Submodel2: ObservableObject {
var anyCancellable: [AnyCancellable] = []
#Published var submodels: [Submodel1] = []
init() {
submodels.forEach({ submodel in
anyCancellable.append(submodel.objectWillChange.sink{ [weak self] (_) in
self?.objectWillChange.send()
})
})
}
}

Resources