Swift MVVM Bind with Boxing - swift

I am simply trying to create a weather application with WeatherViewController displaying the tableView with cells, and when the cell is tapped leads to WeatherDetailsViewController.
I am using the boxing way for binding and I am confused if I set Dynamic type in both the model and viewModel in the example below. You will know what I mean.
This is the Boxing Class
class Dynamic<T>: Decodable where T: Decodable {
typealias Listener = (T) -> ()
var listener: Listener?
var value: T {
didSet {
listener?(value)
}
}
func bind(listener: #escaping Listener) {
self.listener = listener
self.listener?(self.value)
}
init(_ value: T) {
self.value = value
}
private enum CodingKeys: CodingKey {
case value
}
}
This is the Weather Model Struct
struct Weather: Decodable {
let date: Dynamic<Int>
let description: Dynamic<String>
let maxTemperature: Dynamic<Double>
private enum CodingKeys: String, CodingKey {
case date = "time"
case description = "summary"
case maxTemperature = "temperatureMax"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
date = try Dynamic(container.decode(Int.self, forKey: .date))
description = try Dynamic(container.decode(String.self, forKey: .description))
maxTemperature = try Dynamic(container.decode(Double.self, forKey: .maxTemperature))
}
}
Here is my WeatherListViewModel & WeatherViewModel
Inside my WeatherViewModel I have assigned the type to be Dynamic but also in the model in order to bind in my WeatherDetailsViewController, is that right?
class WeatherListViewModel {
var weatherViewModels: [WeatherViewModel]
private var sessionProvider: URLSessionProvider
init(sessionProvider: URLSessionProvider) {
self.sessionProvider = sessionProvider
self.weatherViewModels = [WeatherViewModel]()
}
func numberOfRows(inSection section: Int) -> Int {
return weatherViewModels.count
}
func modelAt(_ index: Int) -> WeatherViewModel {
return weatherViewModels[index]
}
func didSelect(at indexPath: Int) -> WeatherViewModel {
return weatherViewModels[indexPath]
}
}
This is WeatherListViewModel Extension for network fetching where I initialize the WeatherViewModel
func fetchWeatherLocation(withLatitude latitude: CLLocationDegrees, longitude: CLLocationDegrees, completion: #escaping handler) {
sessionProvider.request(type: WeatherWrapper.self, service: WeatherService.specificLocation, latitude: latitude, longitude: longitude) { [weak self] result in
switch result {
case let .success(weatherWrapper):
let weathers = weatherWrapper.daily.weathers
self?.weatherViewModels = weathers.map {
return WeatherViewModel(weather: $0)
}
completion()
case let .failure(error):
print("Error: \(error)")
}
}
}
This is WeatherViewModel
struct WeatherViewModel {
private(set) var weather: Weather
var temperature: Dynamic<Double>
var date: Dynamic<Int>
var description: Dynamic<String>
init(weather: Weather) {
self.weather = weather
self.temperature = Dynamic(weather.maxTemperature)
self.date = Dynamic(weather.date)
self.description = Dynamic(weather.description)
}
}
Here is my WeatherDetailsViewController
Here I assign the binding to the labels respectively to get the changes
class WeatherDetailsViewController: UIViewController {
#IBOutlet private var imageView: UIImageView!
#IBOutlet private var cityLabel: UILabel!
#IBOutlet private var dateLabel: UILabel!
#IBOutlet private var descriptionLabel: UILabel!
#IBOutlet private var temperatureLabel: UILabel!
var viewModel: WeatherViewModel?
override func viewDidLoad() {
super.viewDidLoad()
setupVMBinding()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationItem.largeTitleDisplayMode = .never
}
private func setupVMBinding() {
if let viewModel = viewModel {
viewModel.date.bind {
self.dateLabel.text = $0.toString()
}
viewModel.temperature.bind {
self.temperatureLabel.text = "\($0)"
}
viewModel.description.bind {
self.descriptionLabel.text = $0.description
}
}
}
}
Question is, did I just repeat writing the type Dynamic in both model and viewModel? Is there a better way of doing this or am I on the right track. Sorry for the long code example.

I think you repeat writing Dynamic inside your Weather Model.
It does not need to be Dynamic type.
You can create a GenericDataSource
class GenericDataSource<T>: NSObject {
var data: Dynamic<T>?
}
Inside your View Model. This will Reference to your Weather Model without the need for creating dynamic type.
class WeatherViewModel {
var dataSource: GenericDataSource<Weather>?
....
}
Inside your View Controller
class WeatherDetailsViewController {
var viewModel: WeatherViewModel?
override func viewDidLoad() {
viewModel = ViewModel()
var dataSource = GenericDataSource<Weather>()
dataSource.data = Dynamic(Weather)
viewModel.dataSource = dataSource
setupVMBinding()
}
private func setupVMBinding() {
viewModel?.dataSource?.data?.bind {
self.dateLabel.text = $0.date
self.temperatureLabel.text = "\($0.maxTemperature)"
self.descriptionLabel.text = $0.description
}
}
}

Related

How to tell another view controller what song to play that is listed on a table view?

In my PlayerViewController, I am trying to play a song that is selected from my Table view. Also this first line of code is in another view controller. What would be a better way to write the player.playStream line?
EDIT: PlayerViewController
#IBOutlet weak var playPauseButton: UIButton!
var player: Player!
var song: [Song] = []
let indexPath = IndexPath()
var MusicPlayer: AVAudioPlayer!
override func viewDidLoad() {
super.viewDidLoad()
player = Player()
setSession()
UIApplication.shared.beginReceivingRemoteControlEvents()
becomeFirstResponder()
NotificationCenter.default.addObserver(self, selector: Selector(("handleInterruption")), name: AVAudioSession.interruptionNotification, object: nil)
player.playStream(fileUrl: "http://127.0.0.1/musicfiles" + song[indexPath.row].getName())
changePlayButton()
}
Song.swift:
class Song {
var id = Int()
var name = String()
var year = Int()
var numLikes = Int()
var numPlays = Int()
init?(id:String, name:String, year:String, numlikes:String, numplays:String) {
self.id = Int(id)!
self.name = name
self.year = Int(year)!
self.numLikes = Int(numLikes)
self.numPlays = Int(numplays)!
}
func getId() -> Int {
return id
}
func getName() -> String {
return name
}
func getYear() -> Int {
return year
}
func getNumLikes() -> Int {
return numLikes
}
func getNumPlays() -> Int {
return numPlays
}
}

Bind uitextfield value to viewModel using mvvm

I'm trying to bind a UITextField to a viewModel, however whatever i do i keep getting Cannot invoke 'bind' with an argument list of type '(to: EmailViewModel). What am i doing wrong?
SignUpViewModel
class SignUpViewModel {
let model: SignUpModel
private let disposeBag = DisposeBag()
let emailFieldViewModel = EmailViewModel()
init(model :SignUpModel) {
self.model = model
}
}
EmailViewModel
struct EmailViewModel : FieldViewModel {
var value: Variable<String> = Variable("")
var errorValue: Variable<String?> = Variable(nil)
let title = "Email"
let errorMessage = "Email is wrong"
func validate() -> Bool {
let emailPattern = "[A-Z0-9a-z._%+-]+#([A-Za-z0-9.-]{2,64})+\\.[A-Za-z]{2,64}"
guard validateString(value.value, pattern:emailPattern) else {
errorValue.value = errorMessage
return false
}
errorValue.value = nil
return true
}
}
viewcontroller
class SignUpViewController: UIViewController {
#IBOutlet var emailField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
configureBinding()
}
private func configureBinding() {
// binding
self.emailField.rx.text.bind(to: viewModel.emailFieldViewModel)
}
}
The bind function expects an object that conforms to the ObserverType protocol. Here, EmailViewModel does not conform to that type, hence the error.
Writing an extension to make EmailViewModel conform to the ObserverType protocol would solve the compilation error.
extension EmailViewModel: ObserverType {
func on(_ event: Event<String?>) {
switch event {
case .next(let newValue): value.value = newValue ?? ""
case .error(_), .completed: fatalError("Completion and error are not handled")
}
}
}

How would I write a test to make sure the UIbutton "Show all Providers" turns up when there's more than 12 or more items in the table view?

So I'm completely new to testing and I just needed some help figuring out for example how I would write a test for each of the three cases in the enum of the View Model (none, dontSeeProvider, showAllProviders).
enum ProvidersButtonType {
case none, dontSeeProvider, showAllProviders
}
I haven't been able to figure out how to write a test for cases "showAllProviders" and "dontSeeProviders".
This is the View Model:
import RxSwift
import RxCocoa
struct TopProvidersPickerItem {
let provider: MVPD
let logoImage: Observable<UIImage>
init(provider: MVPD, imageLoader: DecodableProviding) {
self.init(provider: provider, logoImage: imageLoader.image(fromURL: provider.logoUrl))
}
init(provider: MVPD, logoImage: Observable<UIImage>) {
self.provider = provider
self.logoImage = logoImage.catchErrorJustReturn(UIImage())
}
}
enum ProvidersButtonType {
case none, dontSeeProvider, showAllProviders
}
struct TopProvidersPickerViewModel {
var caption: String {
return "Get access to more full episodes by signing in with your TV Provider"
}
let buttonType = Variable<ProvidersButtonType>(.none)
let items: Observable<[TopProvidersPickerItem]>
let selectedItem: PublishSubject<TopProvidersPickerItem> = PublishSubject()
let showAllProvidersTrigger: PublishSubject<Void> = PublishSubject()
let mvpdPicked: Observable<MVPD>
init(topProviders: Observable<[MVPD]>, imageLoader: DecodableProviding) {
let items = topProviders.map({ mvpds in
return mvpds.map { mvpd in
TopProvidersPickerItem(provider: mvpd, imageLoader: imageLoader)
}
})
self.init(items: items)
}
init(items: Observable<[TopProvidersPickerItem]>) {
self.items = items
mvpdPicked = selectedItem.map { $0.provider }
let buttonType = items.map { (array) -> ProvidersButtonType in
if array.count > 12 {
return .showAllProviders
} else {
return .dontSeeProvider
}
}
buttonType.bind(to: self.buttonType)
}
}
This is the View Controller:
import UIKit
import RxCocoa
import RxSwift
public class ProviderCollectionViewCell: UICollectionViewCell {
#IBOutlet public private(set) weak var imageView: UIImageView!
}
public class TopProvidersPickerViewController: UIViewController,
ViewModelHolder {
var viewModel: TopProvidersPickerViewModel! = nil
private let bag = DisposeBag()
#IBOutlet public private(set) weak var collectionView: UICollectionView!
#IBOutlet public private(set) weak var captionLabel: UILabel!
#IBOutlet weak var viewAllProvidersButton: UIButton!
override public func viewDidLoad() {
super.viewDidLoad()
captionLabel.text = viewModel.caption
setupRx()
}
private func setupRx() {
viewModel.buttonType.asObservable().subscribe(onNext: { [button = self.viewAllProvidersButton] type in
button?.isHidden = false
switch type {
case .none:
button?.isHidden = true
case .dontSeeProvider:
button?.setTitle("Don't see provider", for: .normal)
case .showAllProviders:
button?.setTitle("Show all providers", for: .normal)
}
})
.disposed(by: bag)
viewModel.items
.bind(to: collectionView
.rx
.items(cellIdentifier: "ProviderCell", cellType: ProviderCollectionViewCell.self)) { [ unowned self ] _, item, cell in
item.logoImage.bind(to: cell.imageView.rx.image).addDisposableTo(self.bag)
}
.addDisposableTo(bag)
collectionView
.rx
.modelSelected(TopProvidersPickerItem.self)
.bind(to: self.viewModel.selectedItem)
.addDisposableTo(bag)
viewAllProvidersButton
.rx
.tap
.bind(to: self.viewModel.showAllProvidersTrigger)
.addDisposableTo(bag)
}
}
I wrote a test for the "none" case, but haven't been able to figure out the other two cases:
import FBSnapshotTestCase
import OHHTTPStubs
import RxSwift
#testable import AuthSuite
class TopProvidersPickerViewControllerTests: FBSnapshotTestCase,
ProvidersViewControllerTests {
override func setUp() {
super.setUp()
recordMode = true
}
func testDoesNotShowButtonWhenLoadingProviders() {
let viewModel = TopProvidersPickerViewModel(items: .never())
let controller = TopProvidersPickerViewController.instantiateViewController(with: viewModel)
presentViewController(controller)
FBSnapshotVerifyView(controller.view)
}
I've never used FB Snapshot Tester. I'm going to have to look into that.
Here's how I would do it:
I wouldn't expose the enum to the ViewController. setupRx() would contain this instead:
private func setupRx() {
viewModel.buttonTitle
.bind(to: viewAllProvidersButton.rx.title(for: .normal))
.disposed(by: bag)
viewModel.buttonHidden
.bind(to: viewAllProvidersButton.rx.isHidden)
.disposed(by: bag)
// everything else
}
Then to test the title of the button, for example, I would use these tests:
import XCTest
import RxSwift
#testable import RxPlayground
class TopProvidersPickerViewModelTests: XCTestCase {
func testButtonTitleEmptyItems() {
let topProviders = Observable<[MVPD]>.just([])
let decodableProviding = MockDecodableProviding()
let viewModel = TopProvidersPickerViewModel(topProviders: topProviders, imageLoader: decodableProviding)
var title: String = ""
_ = viewModel.buttonTitle.subscribe(onNext: { title = $0 })
XCTAssertEqual(title, "Don't see provider")
}
func testButtonTitle12Items() {
let topProviders = Observable<[MVPD]>.just(Array(repeating: MVPD(), count: 12))
let decodableProviding = MockDecodableProviding()
let viewModel = TopProvidersPickerViewModel(topProviders: topProviders, imageLoader: decodableProviding)
var title: String = ""
_ = viewModel.buttonTitle.subscribe(onNext: { title = $0 })
XCTAssertEqual(title, "Don't see provider")
}
func testButtonTitle13Items() {
let topProviders = Observable<[MVPD]>.just(Array(repeating: MVPD(), count: 13))
let decodableProviding = MockDecodableProviding()
let viewModel = TopProvidersPickerViewModel(topProviders: topProviders, imageLoader: decodableProviding)
var title: String = ""
_ = viewModel.buttonTitle.subscribe(onNext: { title = $0 })
XCTAssertEqual(title, "Show all providers")
}
}
class MockDecodableProviding: DecodableProviding {
// nothing needed for these tests.
}

MVVM with realm: Passing Realm-results across threads?

Using Xcode-8.2.1, Swift-3.0.2, RealmSwift-2.2.0, iOS-Simulator-10:
I try applying the MVVM pattern (explained by Steve Scott here) using Realm.
Everything works until the moment (inside the VIEW-part - see below) where I try to access a viewmodel-property. It says: Realm accessed from incorrect thread
How could I still make the MVVM-pattern do its job of separating model, view-model and view but, on the same time, get thread-safety with realm ?
Is there a way to make Realm-results (i.e. Results<BalancesDataEntry>) being passed across threads ??
Here is my code:
(the issue happens at the very bottom, inside the View-part)
// REALM-OBJECT:
import Foundation
import RealmSwift
class BalancesDataEntry: Object {
dynamic var category: String = ""
dynamic var index: Int = 0
}
MODEL:
import Foundation
import RealmSwift
class MVVMCBalancesModel: BalancesModel
{
fileprivate var entries = [BalancesDataEntry]()
let realm = try! Realm()
init() {
self.createDataEntries()
}
fileprivate func createDataEntries() {
let myBalance = BalancesDataEntry()
myBalance.index = 0
myBalance.category = "Love"
try! self.realm.write {
self.realm.deleteAll()
self.realm.add(myBalance)
}
}
func getEntries(_ completionHandler: #escaping (_ entries: [BalancesDataEntry]) -> Void)
{
// Simulate Aysnchronous data access
DispatchQueue.global().async {
let realmThread = try! Realm()
let returnArray: [BalancesDataEntry] = Array(realmThread.objects(BalancesDataEntry.self))
completionHandler(returnArray)
}
}
}
VIEW-MODEL:
import Foundation
import RealmSwift
class MVVMCBalancesViewModel: BalancesViewModel
{
weak var viewDelegate: BalancesViewModelViewDelegate?
weak var coordinatorDelegate: BalancesViewModelCoordinatorDelegate?
fileprivate var entries: [BalancesDataEntry]? {
didSet {
viewDelegate?.entriesDidChange(viewModel: self)
}
}
var model: BalancesModel? {
didSet {
entries = nil;
model?.getEntries({ (myEntries) in
self.entries = myEntries
})
}
}
var title: String {
return "My Balances"
}
var numberOfEntries: Int {
if let entries = entries {
return entries.count
}
return 0
}
func entryAtIndex(_ index: Int) -> BalancesDataEntry?
{
if let entries = entries , entries.count > index {
return entries[index]
}
return nil
}
func useEntryAtIndex(_ index: Int)
{
if let entries = entries, let coordinatorDelegate = coordinatorDelegate , index < entries.count {
coordinatorDelegate.balancesViewModelDidSelectData(self, data: entries[index])
}
}
}
VIEW:
import UIKit
class MVVMCBalancesViewController: UIViewController {
#IBOutlet weak var label1Outlet: UILabel!
#IBOutlet weak var label2Outlet: UILabel!
var viewModel: BalancesViewModel? {
willSet {
viewModel?.viewDelegate = nil
}
didSet {
viewModel?.viewDelegate = self
refreshDisplay()
}
}
var isLoaded: Bool = false
func refreshDisplay()
{
if let viewModel = viewModel , isLoaded {
// !!!!!!! HERE IS THE ISSUE: Realm accessed from incorrect thread !!!!
self.label1Outlet.text = viewModel.entryAtIndex(0)?.category
self.label2Outlet.text = viewModel.entryAtIndex(1)?.category
} else {
}
}
override func viewDidLoad()
{
super.viewDidLoad()
isLoaded = true
refreshDisplay();
}
}
extension MVVMCBalancesViewController: BalancesViewModelViewDelegate
{
func entriesDidChange(viewModel: BalancesViewModel)
{
}
}
You can use ThreadSafeReference to pass Realm's thread-confined types (Object, Results, List, LinkingObjects) to a different thread. The documentation's section on Passing Instances Across Threads contains this example of passing a single instance of an Object subclass across threads:
let person = Person(name: "Jane")
try! realm.write {
realm.add(person)
}
let personRef = ThreadSafeReference(to: person)
DispatchQueue(label: "background").async {
let realm = try! Realm()
guard let person = realm.resolve(personRef) else {
return // person was deleted
}
try! realm.write {
person.name = "Jane Doe"
}
}
It can be used similarly for Results.
I have found a workaround (see below): Maybe you have better solutions - please let me know!
Here is my github-code realm_mvvm_c on github
After introducing a new protocol and making (pretty much everything) conform to it, things worked out.
Here is the protocol called DataEntry:
import Foundation
protocol DataEntry: class {
var idx: Int { get set }
var category: String { get set }
}
Now, make everything conform to it, such as
--> the realm object (i.e. class BalancesDataEntry: Object, DataEntry {...)
--> the getEntries return value (i.e. func getEntries(_ completionHandler: #escaping (_ entries: [DataEntry]) -> Void))
--> the View-Model's entries (i.e. fileprivate var entries: [DataEntry]? {..)
--> all the corresponding Model- and View-Model protocols also need the DataEntry datatype (see git-repo for complete picture)
After that, it was enough to change the completion-handler return-array of the MODEL's method getEntries(..) to a newly created object-instance (ie. DataEntryDub) that is keept conform to the DataEntry protocol:
func getEntries(_ completionHandler: #escaping (_ entries: [DataEntry]) -> Void)
{
// Simulate Aysnchronous data access
DispatchQueue.global().async {
let realmThread = try! Realm()
class DataEntryDub: DataEntry {
var idx: Int
var category: String
init(idx: Int, category: String) {
self.idx = idx
self.category = category
}
}
var returnArray = [DataEntry]()
for entry in realmThread.objects(BalancesDataEntry.self) {
returnArray.append(DataEntryDub(idx: entry.idx, category: entry.category))
}
completionHandler(returnArray)
}
}
Here is my github-code realm_mvvm_c on github

What does 'Type not allowed here' mean?

I'm using Swift 2.3 and trying to create a callback function that will cause my UIViewController to update itself with the updated view model but I'm getting a compile error in my view model class - 'Type not allowed here'. I'm getting other errors as well but they all seem to be cause by a fundamental problem with my CalculatorViewModel class. It's worth noting that I'm following an example of MVVM used in this great post about iOS architecture patterns and trying to adapt it to my app.
Here's my view controller:
class CalculatorViewController: UIViewController, UITextFieldDelegate, DismissalDelegate {
var viewModel: CalculatorViewModelProtocol! {
didSet {
self.viewModel.oneRepMaxDidChange = { [unowned self] viewModel in
self.oneRepMaxField.text = String(viewModel.oneRepMax!)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = CalculatorViewModel() // use of unresolved identifier 'CalculatarViewModel'
self.viewModel = viewModel
liftNameButton.setTitle(viewModel.liftName, forState: .Normal)
weightLiftedField.text = String(viewModel.weightLifted)
repetitionsField.text = String(viewModel.repetitions)
units.text = viewModel.units
oneRepMaxField.text = String(viewModel.oneRepMax!)
// a bunch of formatting code and then I add a target to a button the user will press:
calculateButton.addTarget(self, action: #selector(onCalculateTapped), forControlEvents: UIControlEvents.TouchUpInside)
func onCalculateButtonTapped() {
if let weightLifted = weightLiftedField.text, let repetitions = repetitionsField.text {
// error: Argument passed to call that takes no arguments (except that it does)
viewModel!.calculateOneRepMax(weightLifted, repetitions: repetitions)
//weightPercentages = getPercentages(pureOneRepMax!)
} else {
return
}
and here's my view model and a view model protocol where the 'Type not allowed error' appears:
protocol CalculatorViewModelProtocol: class {
var liftName: String? { get }
var weightLifted: Double? { get }
var repetitions: Int? { get }
var oneRepMax: String? { get set }
var oneRepMaxDidChange: ((CalculatorViewModelProtocol) -> ())? { get set }
var units: String? { get }
var date: String? { get }
func calculateOneRepMax()
**// the 'Type not allowed here' error is here**
class CalculatorViewViewModel: CalculatorViewModelProtocol, LiftEventDataManagerDelegate {
let calculator = CalculatorBrain()
private let dataManager = LiftEventDataManager()
var liftName: String?
var weightLifted: String!
var repetitions: String!
var oneRepMax: String? {
didSet {
self.oneRepMaxDidChange?(self)
}
}
var units: String?
var date: String?
var oneRepMaxDidChange: ((CalculatorViewModelProtocol) -> ())?
#objc func calculateOneRepMax(weightLifted: String, repetitions: String) {
let result = calculator.calculateOneRepMax(Double(weightLifted)!, repetitions: UInt8(repetitions)!)
}
init() {
dataManager.requestData(withViewModel: self)
}
}
I've done a lot of searching but haven't found any answers that help.
You can not implement a class inside a protocol. Move your CalculatorViewModel to a separate file or at least outside the scope of the protocol