VIPER architecture using Swift to store data in presenter - swift

So I'm setting up a simple VIPER architecture in Swift.
The Interactor gets some data from an API, and passes the data to the presenter that then passes formatted data to the view.
The presenter will process the data, and just count the number of objects that are downloaded. To do so I have stored a var in the presenter. The question is should I store data in the presenter?
Interactor:
class Interactor {
weak var presenter: Presenter?
func getData() {
ClosureDataManager.shared.fetchBreaches(withURLString: baseUrl + breachesExtensionURL, completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
print(error)
case .success(let breaches):
self.presenter?.dataDidFetch(breaches: breaches)
self.presenter?.dataNumberDidFetch(number: breaches.count)
}
})
}
}
Presenter:
class Presenter {
var wireframe: Wireframe?
var view: ViewController?
var interactor: Interactor?
var dataDownloaded = 0
func viewDidLoad() {
print ("presenter vdl")
}
func loadData() {
interactor?.getData()
}
func dataDidFetch(breaches: [BreachModel]) {
view?.dataReady()
}
func showDetail(with text: String, from view: UIViewController) {
wireframe?.pushToDetail(with: text, from: view)
}
func dataNumberDidFetch(number: Int) {
dataDownloaded += number
view?.showData(number: String(dataDownloaded) )
}
}
View (ViewController)
protocol dataViewProtocol {
func showData(number: String)
}
class ViewController: UIViewController, dataViewProtocol {
#IBOutlet weak var showDetailButton: UIButton!
#IBOutlet weak var dataLabel: UILabel!
// weak here means it won't work
var presenter: Presenter?
#IBAction func buttonPressAction(_ sender: UIButton) {
presenter?.loadData()
}
#IBAction func buttonShowDetailAction(_ sender: UIButton) {
presenter?.showDetail(with: "AAA", from: self)
}
func dataReady() {
showDetailButton.isEnabled = true
}
func showData(number: String) {
dataLabel.text = number
}
override func viewDidLoad() {
super.viewDidLoad()
Wireframe.createViewModule(view: self)
presenter?.viewDidLoad()
}
}
Router (Wireframe)
class Wireframe {
static func createViewModule (view: ViewController) {
let presenterInst = Presenter()
view.presenter = presenterInst
view.presenter?.wireframe = Wireframe()
view.presenter?.view = view
view.presenter?.interactor = Interactor()
view.presenter?.interactor?.presenter = presenterInst
}
}
So should the presenter be used to store the number of objects downloaded?
What have you tried I've implemented the var, as shown above. This is a minimum example of the problem.
What resources have you used I've looked on StackOverflow, and Googled the issue. I can't find an answer, but know I could store the data in the view but I think this is incorrect. I could store the number of data in the Interactor, but this also doesn't seem right. It all seems...to violate separation of concerns...
I won't do your homework / use a different architecture / You should use protocols / Why is there a single protocol in your implementation This isn't homework, it is for my own self - study. There may be other architectures that can be used to do this (and coding to protocols is good practice) but this is about storing a variable in the presenter. I want to know if I should store the variable in the presenter, using VIPER and using Swift. Comments about trivia around the question are seldom helpful if they are about variable names, or the like.
What is the question? I want to know if I can store the number of downloaded data items in the presenter.

Related

Swift MVVM - use protocol to handle viewModel events

I'm trying to use MVVM with delegate protocols. When something changes in the view model I want to trigger it in the view controller.
When I want to use protocols to handle the view model's event on a view controller, I can not set the protocol to the view controller for my view model class.
It gives me the error:
Argument type (SecondViewController) -> () -> SecondViewController does not conform to expected type SecondViewModelEvents
How can I do this the right way?
Here is the code for my view model:
protocol SecondViewModelEvents {
func changeBackground()
}
class SecondViewModel:NSObject {
var events:SecondViewModelEvents?
init(del:SecondViewModelEvents) {
self.events = del
}
func loadDataFromServer() {
self.events?.changeBackground()
}
}
And for my view controller class:
class SecondViewController: UIViewController,SecondViewModelEvents {
let viewModel = SecondViewModel(del: self) //Argument type '(SecondViewController) -> () -> SecondViewController' does not conform to expected type 'SecondViewModelEvents'
#IBAction func buttonPressed(_ sender: Any) {
self.viewModel.loadDataFromServer()
}
func changeBackground() {
self.view.backgroundColor = UIColor.red
}
}
You're trying to initialize the view model variable and pass the view controller as a delegate which at this point is not fully initialized.
Try checking out the very informative and very detailed Initialization page in the official Swift language guide.
Since this is a protocol used for this specific purpose, we can safely constrain it to classes (notice the : class addition to your code.
protocol SecondViewModelEvents: class {
func changeBackground()
}
It's good practice to use more descriptive naming, and also using weak references for delegate objects in order to avoid strong reference cycles.
class SecondViewModel {
weak var delegate: SecondViewModelEvents?
init(delegate: SecondViewModelEvents) {
self.delegate = delegate
}
func loadDataFromServer() {
delegate?.changeBackground()
}
}
You can try to use an optional view model, which will get initialized in an appropriate place, like awakeFromNib():
class SecondViewController: UIViewController, SecondViewModelEvents {
var viewModel: SecondViewModel?
override func awakeFromNib() {
super.awakeFromNib()
viewModel = SecondViewModel(delegate: self)
}
#IBAction func buttonPressed(_ sender: Any) {
viewModel?.loadDataFromServer()
}
func changeBackground() {
view.backgroundColor = UIColor.red
}
}
Or an alternative approach would be to initialize a non-optional view model in the UIViewController required initializer:
// ...
var viewModel: SecondViewModel
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.viewModel = SecondViewModel(delegate: self)
}
// ...
You need to use lazy initialization as,
lazy var viewModel = SecondViewModel(del: self)
OR
lazy var viewModel = { [unowned self] in SecondViewModel(del: self) }()

How to update variable in MVVM?

I am trying to use MVVM. I am going to VC2 from VC1. I am updating the viewModel.fromVC = 1, but the value is not updating in the VC2.
Here is what I mean:
There is a viewModel, in it there is a var fromVC = Int(). Now, in vc1, I am calling the viewModel as
let viewModel = viewModel().
Now, on the tap of button, I am updating the viewModel.fromVC = 8. And, moving to the next screen. In the next screen, when I print fromVC then I get the value as 0 instead of 8.
This is how the VC2 looks like
class VC2 {
let viewModel = viewModel()
func abc() {
print(viewModel.fromVC)
}
}
Now, I am calling abc() in viewDidLoad and the fromVC is printed as 0 instead of 8. Any help?
For the MVVM pattern you need to understand that it's a layer split in 2 different parts: Inputs & Outputs.
Int terms of inputs, your viewModel needs to catch every event from the viewController, and for the Outputs, this is the way were the viewModel will send data (correctly formatted) to the viewController.
So basically, if we have a viewController like this:
final class HomeViewController: UIViewController {
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
titleLabel.text = "toto"
}
}
We need to extract the responsibilities to a viewModel, since the viewController is handling the touchUp event, and owning the data to bring to th label.
By Extracting this, you will keep the responsibility correctly decided and after all, you'll be able to test your viewModel correctly 🙌
So how to do it? Easy, let's take a look to our futur viewModel:
final class HomeViewModel {
// MARK: - Private properties
private let title: String
// MARK: - Initializer
init(title: String) {
self.title = title
}
// MARK: - Outputs
var titleText: ((String) -> Void)?
// MARK: - Inputs
func viewDidLoad() {
titleText?("")
}
func buttonDidPress() {
titleText?(title)
}
}
So now, by doing this, you are keeping safe the different responsibilities, let's see how to bind our viewModel to our previous viewController :
final class HomeViewController: UIViewController {
// MARK: - public var
var viewModel: HomeViewModel!
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
// MARK: - Private func
private func bind(to viewModel: HomeViewModel) {
viewModel.titleText = { [weak self] title in
self?.titleLabel.text = title
}
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
viewModel.buttonDidPress()
}
}
So one thing is missing, you'll asking me "but how to initialise our viewModel inside the viewController?"
Basically you should once again extract responsibilities, you could have a Screens layer which would have the responsibility to create the view like this:
final class Screens {
// MARK: - Properties
private let storyboard = UIStoryboard(name: StoryboardName, bundle: Bundle(for: Screens.self))
// MARK: - Home View Controller
func createHomeViewController(with title: String) -> HomeViewController {
let viewModel = HomeViewModel(title: title)
let viewController = storyboard.instantiateViewController(withIdentifier: "Home") as! HomeViewController
viewController.viewModel = viewModel
return viewController
}
}
And finally do something like this:
let screens = Screens()
let homeViewController = screens.createHomeViewController(with: "Toto")
But the main subject was to bring the possibility to test it correctly, so how to do it? very easy!
import XCTest
#testable import mvvmApp
final class HomeViewModelTests: XCTestCase {
func testGivenAHomeViewModel_WhenViewDidLoad_titleLabelTextIsEmpty() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
viewModel.titleText = { title in
XCTAssertEqual(title, "")
expectation.fulfill()
}
viewModel.viewDidLoad()
waitForExpectations(timeout: 1.0, handler: nil)
}
func testGivenAHomeViewModel_WhenButtonDidPress_titleLabelTextIsCorrectlyReturned() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
var counter = 0
viewModel.titleText = { title in
if counter == 1 {
XCTAssertEqual(title, "toto")
expectation.fulfill()
}
counter += 1
}
viewModel.viewDidLoad()
viewModel.buttonDidPress()
waitForExpectations(timeout: 1.0, handler: nil)
}
}
And that's it 💪

Once set up, how are NSSpeechSynthesizerDelegate protocol methods called automatically?

I'm working through a tutorial that uses NSSpeechSynthesizer and two of its NSSpeechSynthesizerDelegate protocol methods. In my ViewController, I don't explicitly call the protocol methods so I'm curious as to what do I need to research in order to understand how these methods are called during runtime? The delegate methods are working as expected but I'm wondering how are they being called which makes this possible?
import Cocoa
class MainWindowController: NSWindowController, NSSpeechSynthesizerDelegate, NSWindowDelegate {
//Now MainWindowController is more powerful by having its own KITT being able to delegate powerful functionality and do less work. The delegate will do all the heavy lifting and return the results to MainWindowController instances.
// MARK: - Properties
#IBOutlet weak var textField: NSTextField!
#IBOutlet weak var speakButton: NSButton!
#IBOutlet weak var stopButton: NSButton!
let speechSynth = NSSpeechSynthesizer.init(voice: NSSpeechSynthesizer.VoiceName.init(rawValue: "com.apple.speech.synthesis.voice.Victoria"))
var isSpeaking: Bool = false {
didSet {
updateButtons()
}
}
// MARK: - Overriden Properties
override var windowNibName: NSNib.Name? {
return NSNib.Name("MainWindowController")
}
// MARK: - Overidden Methods
override func windowDidLoad() {
super.windowDidLoad()
updateButtons()
speechSynth?.delegate = self
}
// MARK: - UI methods
#IBAction func speakIt(sender: NSButton) {
//Get tuype-in text as a string
let string = textField.stringValue
if string.isEmpty {
print("string from \(textField) is empty")
} else {
speechSynth?.startSpeaking(string)
isSpeaking = true
}
}
#IBAction func stopIt(sender: NSButton) {
speechSynth?.stopSpeaking()
}
func updateButtons(){
if isSpeaking {
speakButton.isEnabled = false
stopButton.isEnabled = true
} else {
speakButton.isEnabled = true
stopButton.isEnabled = false
}
}
// MARK: - NSSpeechSynthesizerDelegate Methods
//this functionality is considered more powerful and is made possible due to the speechSynthesizer.delegate = self
//the delegate is doing the work and reporting that completed work to the MainWindowController instance
//so kinda like the delegate is providing the signature and its up to us as the developers based on what we do with those parameters inside the function in order for us to add our own creativity.
func speechSynthesizer(_ sender: NSSpeechSynthesizer, didFinishSpeaking finishedSpeaking: Bool) {
//by setting this variable to FALSE, it will fire off the didSet computed property which this variable has both storage and behavior.
isSpeaking = false
}
// MARK: - NSWindowDelegate Methods
func windowShouldClose(_ sender: NSWindow) -> Bool {
return !isSpeaking
}
}
Your windowDidLoad method contains this line:
speechSynth?.delegate = self
This means the speech synthesizer object has a reference back to your MainWindowController, so the speech synthesizer object can send messages to your MainWindowController.
A simplified implementation inside NSSpeechSynthesizer could look something like this in Swift:
class NSSpeechSynthesizer: NSSoundDelegate {
weak var delegate: NSSpeechSynthesizerDelegate?
func startSpeaking(_ string: String) {
guard
let audioData = audioData(for: string),
let sound = NSSound(data: audioData)
else { return }
sound.delegate = self
sound.play()
}
// Part of NSSoundDelegate
func sound(_ sound: NSSound, didFinishPlaying finished: Bool) {
// The first ? means Swift only sends the message if
// delegate is not nil.
// The second ? means Swift only sends the message if delegate
// implements speechSynthesizer(_:didFinishSpeaking:).
delegate?.speechSynthesizer?(self, didFinishSpeaking: finished)
}
}
But it's actually implemented in Objective-C, where you have to be more verbose about checking whether the delegate handles the message:
- (void)sound:(NSSound *)sound didFinishPlaying:(BOOL)finished {
if ([delegate respondsToSelector:#selector(speechSynthesizer:didFinishSpeaking:)]) {
[delegate speechSynthesizer:self didFinishSpeaking:finished];
}
}

How to set up a NSComboBox using prepare for segue or init?

I am really struggling to pass the contents of one array from a view controller to another to set up the contents of a nscombobox. I have tried everything I can think of, prepare for segue, init; but nothing seems to work.
the program flow is as follows: the user enter a number into a text field and based on it an array with the size of the number is created. Once the user presses a button the next VC appears that has a combo box and inside that combo box those numbers need to appear. All my attempts result in an empty array being passed. Could someone please take a bit of time and help me out. Im sure I'm doing a silly mistake but cannot figure out what.
Code listing below:
Class that take the user input. At this stage I'm trying to pass the contents of the array in the next class as I gave up on prepare for segue because that one crashes because of nil error. Please note that prepare for segue is uncommented in the code listing just for formatting purposes here. Im my program it is commented out as I am using perform segue at the moment.
Any solution would be nice please. Thank you.
import Cocoa
class SetNumberOfFloorsVC: NSViewController {
//MARK: - Properties
#IBOutlet internal weak var declaredNumber: NSTextField!
internal var declaredFloorsArray = [String]()
private var floorValue: Int {
get {
return Int(declaredNumber.stringValue)!
}
}
//MARK: - Actions
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction private func setNumberOfFloors(_ sender: NSButton) {
if declaredNumber.stringValue.isEmpty {
let screenAlert = NSAlert.init()
screenAlert.messageText = "Please specify the number of floors!"
screenAlert.addButton(withTitle: "Got it!")
screenAlert.runModal()
} else if floorValue == 0 || floorValue < 0 {
let screenAlert = NSAlert.init()
screenAlert.messageText = "Please input a correct number of floors!"
screenAlert.addButton(withTitle: "Got it!")
screenAlert.runModal()
} else {
for i in 0...floorValue - 1 {
declaredFloorsArray.append(String(i))
}
print("\(declaredFloorsArray)")
let declareNumberOfRoomsVC = SetNumberOfRoomsForFloorVC(boxData: declaredFloorsArray)
declareNumberOfRoomsVC.boxData = declaredFloorsArray
performSegue(withIdentifier: "set number of rooms", sender: self)
}
}
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
if segue.identifier == "set number of rooms" {
if let addRoomsVC = segue.destinationController as? SetNumberOfRoomsForFloorVC {
addRoomsVC.floorBox.addItems(withObjectValues: declaredFloorsArray)
}
}
}
}
this is the class for the next VC with the combo box:
import Cocoa
class SetNumberOfRoomsForFloorVC: NSViewController, NSComboBoxDelegate, NSComboBoxDataSource {
//MARK: - Properties
#IBOutlet internal weak var floorBox: NSComboBox!
#IBOutlet private weak var numberOfRoomsTxtField: NSTextField!
internal var boxData = [String]()
//MARK: - Init
convenience init(boxData: [String]) {
self.init()
self.boxData = boxData
}
//MARK: - Actions
override func viewDidLoad() {
super.viewDidLoad()
floorBox.usesDataSource = true
floorBox.dataSource = self
floorBox.delegate = self
print("\(boxData)")
}
#IBAction private func setRoomsForFloor(_ sender: NSButton) {
}
//MARK: - Delegates
func numberOfItems(in comboBox: NSComboBox) -> Int {
return boxData.count
}
func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? {
return boxData[index]
}
}
First you should remove the following code.
let declareNumberOfRoomsVC = SetNumberOfRoomsForFloorVC(boxData: declaredFloorsArray)
declareNumberOfRoomsVC.boxData = declaredFloorsArray
I assume you think that the viewController you created here is passed to prepareForSegue. However the storyboard instantiates a new viewController for you.
After that you need to set your declaredFloorsArray as the the boxData of the new viewController in prepareForSegue and you should be good to go.

How to pass value from NSViewController to custom NSView of NSPopover?

By using the delegation protocol I have tried to pass a string (inputFromUser.string) from NSViewController - mainController to custom subclass of NSView of NSPopover - PlasmidMapView, to drawRect function, see code below. But, it didn’t work. I don’t know where a mistake is. Maybe there is another way to pass this string.
Update
File 1.
protocol PlasmidMapDelegate {
func giveDataForPLasmidMap(dna: String)
}
class MainController: NSViewController {
#IBOutlet var inputFromUser: NSTextView!
var delegate: plasmidMapDelegate?
#IBAction func actionPopoverPlasmidMap(sender: AnyObject) {
popoverPlasmidMap.showRelativeToRect(sender.bounds,
ofView: sender as! NSView, preferredEdge: NSRectEdge.MinY)
let dna = inputDnaFromUser.string
delegate?.giveDataForPLasmidMap(dna!)
}
}
File 2
class PlasmidMapView: NSView, PlasmidMapDelegate {
var dnaForMap = String()
func giveDataForPLasmidMap(dna: String) {
dnaForMap = dna
}
override func drawRect(dirtyRect: NSRect) {
let objectOfMainController = MainController()
objectOfMainController.delegate = self
//here I have checked if the string dnaForMap is passed
let lengthOfString = CGFloat(dnaForMap.characters.count / 10)
let pathRect = NSInsetRect(self.bounds, 10, 45)
let path = NSBezierPath(roundedRect: pathRect,
xRadius: 5, yRadius: 5)
path.lineWidth = lengthOfString //the thickness of the line should vary in dependence on the number of typed letter in the NSTextView window - inputDnaFromUser
NSColor.lightGrayColor().setStroke()
path.stroke()
}
}
Ok, there's some architecture mistakes. You don't need delegate method and protocol at all. All you just need is well defined setter method:
I. Place your PlasmidMapView into NSViewController-subclass. This view controller must be set as contentViewController-property of your NSPopover-control. Don't forget to set it the way you need in viewDidLoad-method or another.
class PlasmidMapController : NSViewController {
weak var mapView: PlacmidMapView!
}
II. In your PlacmidMapView don't forget to call needsDisplay-method on dna did set:
class PlasmidMapView: NSView {
//...
var dnaForMap = String() {
didSet {
needsDisplay()
}
//...
}
III. Set dna-string whenever you need from your MainController-class.
#IBAction func actionPopoverPlasmidMap(sender: AnyObject) {
popoverPlasmidMap.showRelativeToRect(sender.bounds,
ofView: sender as! NSView, preferredEdge: NSRectEdge.MinY)
let dna = inputDnaFromUser.string
if let controller = popoverPlasmidMap.contentViewController as? PlasmidMapController {
controller.mapView.dna = dna
} else {
fatalError("Invalid popover content view controller")
}
}
In order to use delegation your class PlasmidMapView needs to have an instance of the MainController (btw name convention is Class, not class) and conform to the PlasmidMapDelegate (once again name convention dictates that it should be PlasmidMapDelegate). With that instance you then can:
mainController.delegate = self
So, after several days I have found a solution without any protocols and delegation as Astoria has mentioned. All what I needed to do was to make #IBOutlet var plasmidMapIBOutlet: PlasmidMapView!for my custom NSView in MainController class and then to use it to set the value for the dnaForMap in #IBAction func actionPopoverPlasmidMap(sender: AnyObject).
class PlasmidMapView: NSView
{
var dnaForMap = String()
}
class MainController: NSViewController
{
#IBOutlet var inputFromUser: NSTextView!
#IBOutlet var plasmidMapIBOutlet: PlasmidMapView!
#IBAction func actionPopoverPlasmidMap(sender: AnyObject)
{
plasmidMapIBOutlet.dnaForMap = inputDnaFromUser.string!
popoverPlasmidMap.showRelativeToRect(sender.bounds,
ofView: sender as! NSView, preferredEdge: NSRectEdge.MinY)
}
}