I'm having trouble using ReactiveCocoa in version 3. I want to build some view model for my login view controller. In my view controller I have outlet for password text field:
#IBOutlet weak var passwordTextField: UITextField!
In view model I have property for the text that is the password
public let emailText = MutableProperty<String>("")
and the question is how to bind it together? I'm able to get SignalProducer from text field:
emailTextField.rac_textSignal().toSignalProducer()
but how to bind it to emailText property? I've read in documentation that SignalProducer is not a Signal, but it can create now. There is method start() but it takes Sink as parameter and I'm a bit confused with design at this moment. Shouldn't emailText be a Sink?
Note: this is not properly an answer to your question, but I think it might help you.
If you are just want to bind your view to your view model, I suggest you to read this post which provides a one-class solution to the problem.
From there, you can very simply implement a 2-way binding so your viewmodel get updated every time the view changes and vice-versa. Here is my extension:
class TwoWayDynamic<T> {
typealias Listener = T -> Void
private var viewListener: Listener?
private var controllerListener: Listener?
private(set) var value: T
func setValueFromController(value: T) {
self.value = value
viewListener?(value)
}
func setValueFromView(value: T) {
self.value = value
controllerListener?(value)
}
func setValue(value: T) {
self.value = value
controllerListener?(value)
viewListener?(value)
}
init(_ v: T) {
value = v
}
func bindView(listener: Listener?) {
self.viewListener = listener
}
func bindController(listener: Listener?) {
self.controllerListener = listener
}
func bindViewAndFire(listener: Listener?) {
self.viewListener = listener
listener?(value)
}
func bindControllerAndFire(listener: Listener?) {
self.controllerListener = listener
listener?(value)
}
}
Hope it helps!
Related
I'm learning how to use MVVM to make a little project recently, and I've encountered some problems when I want to store the information.
Is it possible to make this Observable class conform to codable? Because when I tried to use userDefault to store a view model array in my home page, the system warned me that the view model does not conform to codable. However, when I go to my view model and mark this one var badHabitInfo: Observable<BadHabitModel> = Observable(nil), the warning disappeared. So I think if I make the Observable class conform to codable, it should be ok to encode/decode my view model array.
Does anyone can help? Thanks in advance!
class Observable<T> {
var value: T? {
didSet{
listener?(value)
}
}
init(_ value: T?) {
self.value = value
}
private var listener: ((T?) -> Void)?
func bind(listener: #escaping (T?) -> Void) {
listener(value)
self.listener = listener
}
}
This is my view model array in the home page.
var badHabits: [AddQuitItemViewModel] = []
I am developing a state management library. The original design only has 1 listener, which works great until I need to support multiple listeners.
The original design is here:
Swift how to use generic protocol in generic class
This is what I have done to support multiple listeners:
public protocol StateObserver: AnyObject {
associatedtype State
func didUpdateState(_ state: State)
}
public final class StateStore<Observer: StateObserver> {
struct WeakRef<T: AnyObject> {
weak var value: T?
}
public private(set) var state: Observer.State
private var observers = [WeakRef<Observer>]()
public init(initialState: Observer.State) {
state = initialState
}
public func addObservers(_ observers: [Observer]) {
self.observers += observers.map { WeakRef(value: $0) }
}
public func update(_ block: (inout Observer.State) -> Void) {
var nextState = state
block(&nextState)
state = nextState
notify()
}
public func notify() {
for observer in observers {
observer.value?.didUpdateState(state)
}
}
}
Now I need to create the store with 2 observers:
class MyScene: SKScene {
init {
let leftPanel = LeftPanelSKNode()
let topBar = TopBarSKNode()
let store: StateStore<?> // How to make this support `LeftPanelSKNode `, `TopBarSKNode`, and `MyScene`?
store.addObservers([leftPanel, topBar, self])
}
Now I am stuck here. I need to create a StateStore<?> of something, which can be either MyScene, LeftPanelSKNode and TopBarSKNode.
First of all, I have to say that what you are building already exists in many reactive libraries:
CurrentValueSubject in Apple's Combine;
BehaviorSubject in RxSwift;
You can also check the small internal class I've made myself, it allows to hold the state and observe it ObservableProperty.
Back to your question, I've found a way to add the StateObserver one by one while keeping only the weak reference to them.
public protocol StateObserver: AnyObject {
associatedtype State
func didUpdateState(_ state: State)
}
class Node1: StateObserver {
typealias State = Int
func didUpdateState(_ state: Int) { }
}
class Node2: StateObserver {
typealias State = Int
func didUpdateState(_ state: Int) { }
}
class StateStore<StateType> {
private(set) var state: StateType
init(_ initialState: StateType) {
self.state = initialState
}
private var observers: [(StateType) -> Void] = []
func observe<Observer: StateObserver>(by observer: Observer) where Observer.State == StateType {
weak var weakObserver = observer
observers.append { state in
weakObserver?.didUpdateState(state)
}
}
func notify() {
observers.forEach {
$0(self.state)
}
}
}
let store = StateStore<Int>(0)
let node1 = Node1()
let node2 = Node2()
store.observe(by: node1)
store.observe(by: node2)
Adding the array-based observe API might be a problem because of the associatedtype in the StateObserver.
So now in the app i'm currently developing I decided to refactor it by moving to the MVVM design pattern. And here it is where I got to know the famous "Observables".
I managed to understand how they work and the importance of their existence when using MVVM, I've read a couple of explanations on the different techniques for the implementation. By techniques I mean:
Observables (the one I'm currently using)
Event Bus / Notification Center
FRP Techinque (ReactiveCocoa / RxSwift)
I've declared my Bindable class like this:
import UIKit
class Bindable<T> {
var value: T? {
didSet {
observer?(value)
}
}
var observer: ((T?) -> ())?
func bind(observer: #escaping (T?) -> ()) {
self.observer = observer
}
}
What I wanted to do is to bind 2 UITextField's (that are inside one of my ViewController's) with the respective ViewModel. Inside my ViewController there are 2 textfields (emailInput - passwordInput) and a 'Log In' button, that I want it to be disabled unless both textfields aren't empty.
For that I've added both textfield's this target:
emailInput.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
passwordInput.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
Then:
/// Enable / Disable --> Log In button
#objc func textFieldDidChange(_ textField: UITextField) {
if (emailInput.text == "") || (passwordInput.text == "") {
logInButton.enableButton(false)
} else {
logInButton.enableButton(true)
}
}
But my question is... How could I implement this same thing inside my ViewModel??
And is it possible to do a two-way binding using my Bindable class?
(If more code is needed to solve this, just ask me to and I'll edit the question)
Observable is used to communicate changes from the view model to the view. There is no need for your view model to use the Observable pattern in order to respond to the updates in your text fields. You can provide a simple function setCredentials(email: String, password: String). In this function you can check if those values are empty and set var loginEnabled: Bindable<Bool>. Your view observes the loginEnabled and sets the login button state accordingly.
struct ViewModel {
var loginEnabled = Bindable<Bool>()
var email = ""
var password = ""
init() {
self.loginEnabled.value = false
}
func setCredentials(email: String, password: String) {
self.email = email
self.password = password
self.loginEnabled.value = !email.isEmpty && !password.isEmpty
}
}
Then in your view controller you have something like
var viewModel: ViewModel
override func viewDidLoad {
super.viewDidLoad()
self.viewModel.loginEnabled.bind { value in
self.logInButton.isEnabled = value ?? false
}
}
#objc func textFieldDidChange(_ textField: UITextField) {
self.viewModel.setCredentials(email: self.emailInput.text ?? "", password: self.passwordInput.text ?? "")
}
I have read articles about MVC, MVP and MVVM architecture but I am not cleat about how to create each architecture in my iOS app. Which classes/controller files I need to use to make for each architecture. What is the difference between them if we are using with Storyboard/Xib/Programmatically?
As I am using Xcode default MVC structure for iOS apps but I want to create my new project with MVVM structure but I am not sure how to create that structure.
Any help would be highly appreciated.
Thanks in advance.
This is an oversimplification of the many variants of these design patterns, but this is how I like to think about the differences between the two.
MVC
MVP
MVVM
for more information you can look at here
MVVM architecture in iOS can be easily implemented without using third party dependencies. For data binding, we can use a simple combination of Closure and didSet to avoid third-party dependencies.
public final class Observable<Value> {
private var closure: ((Value) -> ())?
public var value: Value {
didSet { closure?(value) }
}
public init(_ value: Value) {
self.value = value
}
public func observe(_ closure: #escaping (Value) -> Void) {
self.closure = closure
closure(value)
}
}
An example of data binding from ViewController:
final class ExampleViewController: UIViewController {
private func bind(to viewModel: ViewModel) {
viewModel.items.observe(on: self) { [weak self] items in
self?.tableViewController?.items = items
// self?.tableViewController?.items = viewModel.items.value // This would be Momory leak. You can access viewModel only with self?.viewModel
}
// Or in one line:
viewModel.items.observe(on: self) { [weak self] in self?.tableViewController?.items = $0 }
}
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
}
protocol ViewModelInput {
func viewDidLoad()
}
protocol ViewModelOutput {
var items: Observable<[ItemViewModel]> { get }
}
protocol ViewModel: ViewModelInput, ViewModelOutput {}
final class DefaultViewModel: ViewModel {
let items: Observable<[ItemViewModel]> = Observable([])
// Implmentation details...
}
Later it can be replaced with SwiftUI and Combine (when a minimum iOS version in of your app is 13)
In this article, there is a more detailed description of MVVM
https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3
In MVVM, the View (which is really the UIViewController) asks the View Model for information.
textLabel.text = viewModel.textToShow
The view model has a representation of everything needed to construct the UI. The View asks the View Model for those values (like, what string to show).
In MVP, the Presenter tells the View what to do.
view.showText(textToShow)
The view controller implements a protocol that translates this request into view controller specifics:
func showText(_ text: String) {
textLabel.text = text
}
Here's an example of MVP: https://stackoverflow.com/a/54499119/246895.
I am new to Swift and I have trouble using classes and structures.
I have a Structure called Workspace:
struct Workspace: Decodable {
var guid: String
var name: String
func getUserWorkspace(base: String, completed: #escaping () -> ()){
//some code
}
}
Here is my class User:
public class User {
var Wor = [Workspace]()
var WorData:Workspace? = nil
//+some other var & functions
}
So what I'm doing in my view controller is this:
class SecondViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var listView: UITableView!
var co = User()
override func viewDidLoad() {
super.viewDidLoad()
co.WorData?.getUserWorkspace(base: co.Base) {
print("success")
self.listView.reloadData()
self.updateVertically()
}
listView.delegate = self
listView.dataSource = self
}
The problem is that the code never goes inside the function co.WorData?.getUserWorkspace(base: co.Base)
Before I put it in the structure it was directly in the class but since I changed it it doesn't work anymore so I think I might be calling it the wrong way ?
WorData is nil.
Conditional unwrapping (co.WorData?.getUserWorkspace(base: co.Base) will check WorData has a value before trying to call the method. If it was nil and Swift didn't do this, it would crash.
You either need to set it as new all the time
var worData = Workspace()
Set it after class init
var user = User()
user.worData = Workspace() // or pass a specific one in
or require your User object to be initialised with a Workspace
class User: NSObject {
var wor = [Workspace]()
var workspace: Workspace // use lower camel case for var names
required init(workspace: Workspace) {
self.workspace = workspace
}
}