I struggle with learning MVVM architecture. My problem is that I can't send data to the next VC.
App idea: I have 2 view controllers. In first VC user sets own parameteres (height and weight with UISlider). Later app presents second VC where user has got information about BMI.
MODEL
struct BMI {
let value: Float
let advice: String
let color: UIColor
let diagnosis: String
}
VIEW MODEL
protocol BmiViewControllerDelegate: class {
func getCalculatedBMI(newBmi: BMI)
}
protocol BmiViewModelDelegate: class {
func sendValue(height: Float?, weight: Float?)
}
class BmiViewModel: BmiViewModelDelegate {
var bmiModel = BmiCalculator()
var bmi: BMI
weak var delegateVC: BmiViewControllerDelegate?
func sendValue(height: Float?, weight: Float?) {
guard let height = height, let weight = weight else { return }
calculateBmi(height: height, weight: weight)
}
func calculateBmi(height: Float, weight: Float) {
let bmiValue = weight / pow(height, 2)
if bmiValue < 18.5 {
bmi = BMI(value: bmiValue, advice: "You should eat more calories", color: .red, diagnosis: "Underweight")
delegateVC?.getCalculatedBMI(newBmi: bmi!)
} else if bmiValue < 24.9 {
bmi = BMI(value: bmiValue, advice: "Your weight is great! Keep it up!", color: .green, diagnosis: "")
delegateVC?.getCalculatedBMI(newBmi: bmi!)
} else {
bmi = BMI(value: bmiValue, advice: "You should eat less calories", color: .red, diagnosis: "Overweight")
delegateVC?.getCalculatedBMI(newBmi: bmi!)
}
}
}
VIEW CONTROLLER
class BMIViewController: UIViewController {
var bmiViewModel = BmiViewModel()
#IBOutlet weak var heightLabel: UILabel!
#IBOutlet weak var heightSlider: UISlider!
#IBOutlet weak var weightLabel: UILabel!
#IBOutlet weak var weightSlider: UISlider!
#IBOutlet weak var calculateButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
bmiViewModel.delegateVC = self
heightSlider.value = 1.5
weightSlider.value = 80
}
#IBAction func heightSliderChanged(_ sender: UISlider) {
let height = String(format: "%.2f", sender.value)
heightLabel.text = "\(height)m"
}
#IBAction func weightSliderChanged(_ sender: UISlider) {
let weight = String(format: "%.0f", sender.value)
weightLabel.text = "\(weight)kg"
}
#IBAction func calculateButtonTapped(_ sender: UIButton) {
let height = heightSlider.value
let weight = weightSlider.value
bmiViewModel.sendValue(height: height, weight: weight)
}
}
extension BMIViewController: BmiViewControllerDelegate {
func getCalculatedBMI(newBmi: BMI) {
let bmiResult = BMIResultViewController()
bmiResult.bmiValue = String(newBmi.value)
bmiResult.advice = newBmi.advice
bmiResult.diagnosis = newBmi.diagnosis
bmiResult.color = newBmi.color
}
I've tried to print values in getCalculatedBMI and these values exists, so why when I open BMIResultViewController values are empty.
And I have one additional question: if force unwrapped bmi values in "delegateVC?.getCalculatedBMI(newBmi: bmi!)" isn't bad approach?
This code
func getCalculatedBMI(newBmi: BMI) {
let bmiResult = BMIResultViewController()
bmiResult.bmiValue = String(newBmi.value)
bmiResult.advice = newBmi.advice
bmiResult.diagnosis = newBmi.diagnosis
bmiResult.color = newBmi.color
}
Doesn't do what you want it to.
First, it allocates a new BMIResultViewController, but this view controller isn't shown in any way. It will just be thrown away when this function exits.
But this is academic because getCalculatedBMI isn't even called. You have used a delegation pattern, but the instance of BMIResultViewController that is shown by the segue can't invoke it since it doesn't have a model instance with the delegateVC property set.
First, let's fix your model and view model objects
class BMIModel {
var weight: Float = 0 {
didSet: {
calculateBMI()
}
}
var height: Float = 0 {
didSet: {
calculateBMI()
}
}
var bmi: BMI
init() {
self.bmi = BMI(value: 0, advice: "You should eat more calories", color: .red, diagnosis: "Underweight")
}
private func calculateBMI() {
let bmiValue = self.weight / pow(self.height, 2)
if bmiValue < 18.5 {
self.bmi = BMI(value: bmiValue, advice: "You should eat more calories", color: .red, diagnosis: "Underweight")
} else if bmiValue < 24.9 {
self.bmi = BMI(value: bmiValue, advice: "Your weight is great! Keep it up!", color: .green, diagnosis: "")
} else {
self.bmi = BMI(value: bmiValue, advice: "You should eat less calories", color: .red, diagnosis: "Overweight")
}
}
}
struct BmiViewModel {
let bmiModel: BMIModel
func setValues(weight: Float, height: Float) {
self.bmiModel.weight = weight
self.bmiModel.heihght = height
}
}
struct BmiResultViewModel {
let bmiModel: BMIModel
var weight: Float {
return bmiModel.weight
}
var height: Float {
return bmiModel.height
}
var bmiResult: BMI {
return bmiModel.bmi
}
}
You have a final problem in that you have a segue triggered from your button and you are also using an #IBAction from that button to calculate the bmi. It isn't defined in which order these things will happen. In many cases the segue will trigger before the tap handler executes.
You can fix this by changing the storyboard segue to one linked to the view controller that you perform by identifier or you can perform the calculation during the segue.
The former is probably the correct approach:
Remove the segue between the button and the destination
Create a segue between the view controller object itself and the destination
Give the segue an identifier, say showResults
Fix your view model so that it updates the model.
Now you can invoke the segue programmatically.
You need to implement prepare(for:sender) in BMIViewController in order to set properties on the destination view controller.
I wouldn't bother with the delegate pattern. Just set the destination view model.
class BMIViewController: UIViewController {
let bmiViewModel = BmiViewModel(model: BMIModel())
#IBAction func calculateButtonTapped(_ sender: UIButton) {
let height = heightSlider.value
let weight = weightSlider.value
self.bmiViewModel.setValues(weight: weight, height: height)
self.perform(segueWithIdentifier:"showResult", sender:self}
}
func prepare(for segue:UIStoryboardSegue, sender: any?) {
if let destVC = segue.destinationViewController as? BMIResultViewController {
destVC.bmiViewModel = BmiResultViewModel(bmiModel: self.bmiViewModel.bmiModel)
}
}
}
This is just my opinion, but UIKit is inherently MVC. Retrofitting MVVM really only makes sense if you introduce a binding framework (which is what you were trying to do with the delegate pattern).
If you want to learn MVVM, use SwiftUI.
Related
I'm getting an error " Value of type 'SecondViewController' has no member 'bmiValue' "
I'm trying to find out what's wrong with the code
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var heightLabel: UILabel!
#IBOutlet weak var weightLabel: UILabel!
#IBAction func weightSliderChanged(_ sender: UISlider) {
weightLabel.text = String(format: "%.0f", sender.value)
}
#IBAction func heightSliderChanged(_ sender: UISlider) {
heightLabel.text = String(format: "%.2f", sender.value)
}
#IBAction func calculatePressed(_ sender: UIButton) {
let weight = weightSliderValue.value
let height = heightSliderValue.value
let bmi = weight/pow(height, 2)
print(bmi)
let secondVc = SecondViewController() Value of type 'SecondViewController' has no member 'bmiValue'
secondVc.bmiValue = String(format: ".1f", bmi)
self.present(secondVc, animated: true, completion: nil)
}
#IBOutlet weak var weightSliderValue: UISlider!
#IBOutlet weak var heightSliderValue: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
here is the code for second view controller where I want to display bmi
import UIKit
var bmiValue = 0.0
class SecondViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
label.text = "Hello"
label.frame = CGRect(x: 0, y: 0, width: 100, height: 50)
view.addSubview(label)
}
}
If you want to pass a value as a String, the var / property you are setting must also be a String:
class SecondViewController : UIViewController {
// if you want to pass a string
var bmiValue: String = "0.0"
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
label.text = "Hello"
label.frame = CGRect(x: 0, y: 0, width: 100, height: 50)
view.addSubview(label)
}
}
Now, in your "main" view controller, you can do this:
let secondVc = SecondViewController()
secondVc.bmiValue = String(format: ".1f", bmi)
self.present(secondVc, animated: true, completion: nil)
I keep getting "Cannot assign to value: 'calculateBMI' is a method" error message when using a struct property. any way around this. This is my code from the struct:
import UIKit
struct Calculations {
var bmi : Float = 0.0
func getBMIValue() -> String {
let BMIRounded = String(format: "%.1f", bmi)
return BMIRounded
}
mutating func calculateBMI (height: Float, weight: Float) {
bmi = weight / (height * height)
}
}
and this is where I get the error message on my First Page View controller:
import UIKit
class HomeViewController: UIViewController {
var calculations = Calculations()
#IBOutlet weak var heightLabel: UILabel!
#IBOutlet weak var weightLabel: UILabel!
#IBOutlet weak var heightSlider: UISlider!
#IBOutlet weak var weightSlider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func HeightSliderChange(_ sender: UISlider) {
var height = String (format: "%.2f", sender.value)
heightLabel.text = "\(height)m"
print(height)
}
#IBAction func WeightSliderChange(_ sender: UISlider) {
var weight = String (format: "%.0f", sender.value)
weightLabel.text = "\(weight)kg"
}
#IBAction func calculatePressed(_ sender: Any) {
let height = heightSlider.value
let weight = weightSlider.value
calculations.calculateBMI = (height: height, weight: weight)
self.performSegue(withIdentifier: "GettingResults", sender: self)
}
}
The error happens on line 43 (calculations.calculateBMI)
As the error states clearly you are trying to set calculateBMI which is a function and not a variable. To fix this issue modify your calculatePressed method like this:
#IBAction func calculatePressed(_ sender: Any) {
let height = heightSlider.value
let weight = weightSlider.value
calculations.calculateBMI(height: height, weight: weight)
self.performSegue(withIdentifier: "GettingResults", sender: self)
}
I am new in iOS development and want to learn unit testing. I want to add two numbers using text fields and show the result in a label when clicking on the action. Can anyone suggest me how to make a unit test for this?
This is a short example, but you should always try multple different variations as if it would be text instead of numbers and negative numbers, numbers that is higher than int etc...
class AddingViewController: UIViewController {
var sum = 0
let textField1 = UITextField()
let textField2 = UITextField()
let button = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
button.addTarget(self, action: #selector(addNumbers), for: .touchUpInside)
}
#objc func addNumbers() {
guard let number1 = Int(textField1.text!), let number2 = Int(textField2.text!) else {
return
}
sum = number1 + number2
}
}
class TestAddingViewController {
let vc = AddingViewController()
func testAddNumbers() {
let number1 = 1
let number2 = 2
let sum = number1 + number2
vc.textField1.text = "\(number1)"
vc.textField2.text = "\(number2)"
vc.button.sendActions(for: .touchUpInside)
XCTAssertEqual(sum, vc.sum)
}
}
If viewController is from storyboard you call it as you normally call storyboard viewControllers
let storyboard = UIStoryboard(name: "MyStoryboardName", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "someViewController")
And the textfields + button will be #outlets and the button action will be an #IBAction and not #objc func
Here is my view controller class:
import UIKit
class ViewController: UIViewController {
// MARK: ▼▼▼ IBOutlets ▼▼▼
#IBOutlet weak var lblAnswer: UILabel!
// MARK: ▼▼▼ Lifecycle methods ▼▼▼
override func viewDidLoad() {
super.viewDidLoad()
lblAnswer.text = "\(calculateSum(num1: 5, num2: 10))"
}
// MARK: ▼▼▼ Methods ▼▼▼
func calculateSum(num1: Int, num2: Int) -> Int {
return (num1 + num2)
}
}
And for the unit test class I have:
import XCTest
#testable import Test
class TestTests: XCTestCase {
// MARK: ▼▼▼ Variables ▼▼▼
var num1: Int?
var num2: Int?
var testClass: ViewController?
// This is used to initialise your code:
override func setUp() {
testClass = ViewController()
num1 = 10
num2 = 10
}
// This runs when test is complete:
override func tearDown() {
testClass = nil
num1 = nil
num2 = nil
}
// Should begin with test so that it is recognised:
func testSumFunction() {
let sum = testClass!.calculateSum(num1: num1!, num2: num2!)
// The sum should be 20 for this to pass.
XCTAssertTrue(sum == 20)
}
}
If the logic of the function is changed and answer is not equal to 20 then this test will fail.
I'm new to swift and am making an graphing app with two views where one has a text field to enter data and the other has a view to display the data. I've got the data as two arrays of doubles in my ViewController class, but I can't move the data to the class of UIView where I want to draw to the view it because it does not inherit the arrays. I've tried accessor methods but that hasn't changed anything.
Here is the class that contains xValues and yValues
class ViewController: UIViewController {
#IBOutlet var DataEntry: UITextView!
var xValues = Array<Double>()
var yValues = Array<Double>()
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
//the xValues and yValues arrays are filled when the view changes on the press of a button
}
public func getXCord() -> Array<Double>{
return xValues
}
public func getYCord() -> Array<Double>{
return yValues
}
}
Here is the class I want them delivered to. I'm getting an error when initializing xCords and yCords to ViewController.getXCord() and ViewController.getYCord() respectively.
class GraphView: UIView{
var points: [Points] = []
var xCords: Array<Double> = ViewController.getXCord()
var yCords: Array<Double> = ViewController.getYCord()
var position = CGPoint(x: 0,y: 0)
}
You are doing this the complete opposite way.
In the MVC pattern, the view, your GraphView class, should never directly talk to the controller. Instead, the view should use a delegate and/or a datasource to communicate with the controller.
Your view should have a GraphViewDatasource:
protocol GraphViewDatasource : class {
func xValues(inGraph: GraphView) -> [Double]
func yValues(inGraph: GraphView) -> [Double]
}
// in GraphView
weak var datasource: GraphViewDatasource?
func reloadData() {
guard let datasource = self.datasource else { return }
xValues = datasource.xValues(inGraph: self)
yValues = datasource.yValues(inGraph: self)
// redraw the graph...
}
Your controller should implement GraphViewDatasource:
class ViewController: UIViewController, GraphViewDatasource {
func xValues(inGraph: GraphView) -> [Double] { return self.xValues }
func yValues(inGraph: GraphView) -> [Double] { return self.yValues }
}
and set self as the data source of the graph view:
let graph = GraphView(frame ...)
self.view.addSubView(graph)
graph.datasource = self
graph.reloadData()
You need to pass xCoords and yCoords to GraphView from ViewController.
First, initialize xCoords and yCoords with empty array:
class GraphView: UIView{
var points: [Points] = []
var xCords: Array<Double> = []
var yCords: Array<Double> = []
var position = CGPoint(x: 0,y: 0)
}
Than pass it from ViewController:
class ViewContoller: UIViewController {
#IBOutlet var graphView: GraphView!
override func viewDidLoad() {
super.viewDidLoad()
graphView.xCoords = self.xCoords
graphView.yCoords = self.yCoords
}
}
I am working on iOS application in swift 3.0 where I am creating custom view with textfield and button calculate values for all text filed and display the sum of all textfield on the top totalText.
Code for MainViewController:
#IBOutlet weak var totalText: UITextField!
var totalview:[UIView]!
override func viewDidLoad() {
yvalue = 1
tag = 1
count = 1
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func actionButton(_ sender: UIButton) {
yvalue = 55 + yvalue
//for i in 0...count {
extraview = View(frame: CGRect(x: 50, y: 75+yvalue, width: 350, height: 50))
extraview.backgroundColor = UIColor(white: 1, alpha: 0.5)
extraview.layer.cornerRadius = 15
extraview.tag = tag
print("ExtraView tag=",extraview.tag)
extraview.ActionButtonsub.addTarget(self, action: (#selector(cancelbutton(_:))), for: UIControlEvents.touchUpInside)
extraview.textFiled.addTarget(self, action: #selector(didChangeTexts(textField:)), for: .editingChanged)
extraview.textFiled.tag = tag
print("text tag=",extraview.textFiled.tag)
self.view.addSubview(extraview)
count = count + 1
tag = tag + 1
//}
}
func cancelbutton(_ sender: UIButton) {
extraview.removeFromSuperview()
}
func didChangeTexts(textField: UITextField) {
totalText.text = extraview.textFiled.text
}
Code for UIView:
class View: UIView {
#IBOutlet var subView: UIView!
#IBOutlet weak var textFiled: UITextField!
#IBOutlet weak var ActionButtonsub: UIButton!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
Bundle.main.loadNibNamed("View",owner: self, options:nil)
self.addSubview(self.subView)
}
override init(frame: CGRect) {
super.init(frame: frame)
Bundle.main.loadNibNamed("View", owner: self, options: nil)
subView.frame = bounds
self.addSubview(self.subView)
}
}
Sample Output
If you think #Scriptable way is complex then go like below..
Simple way, you can add those textfields into one collection while creating & looping it when you need to do something.
Something like this should work, may need a little modification.
You just need to iterate through the subviews, get the textfield for each view, convert to double value and add them up along the way.
func calculateTotal() -> Double {
var total: Double = 0.0
for subview in self.view.subviews {
if let v = subview as? View, !v.textFiled.isEmpty {
total += Double(v.textFiled.text)
}
}
return total
}
An alternative is applying filter, flatMap and reduce functions
let sum = (view.subviews.filter{$0 is View} as! [View]) // filters all `View` instances
.flatMap{$0.textFiled.text} // maps all text properties != nil
.flatMap{Double($0)} // maps all values which are convertible to Double
.reduce(0.0, {$0 + $1}) // calculates the sum