I am trying to learn binding and understand the MVVM approach in Swift.
I was expecting the below example to work, essentially someEventHappened is called, this invokes the onEvent closure and my message is logged to the screen.
This does not happen however, nothing is printed and I am a little unsure as to why?
class ViewModal {
public var onEvent: (() -> Void)?
func someEventHappened() -> Void {
onEvent?()
}
}
class ViewController: UIViewController {
lazy var viewModel: ViewModal = {
let viewModal = ViewModal()
return viewModal
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.backgroundColor = .purple
viewModel.someEventHappened()
viewModel.onEvent = {
print("something happened")
}
}
}
Just swap assigning onEvent and calling someEventHappened
viewModel.onEvent = {
print("something happened")
}
viewModel.someEventHappened()
this is because you're calling onEvent handler inside someEventHappened and in viewDidLoad you first had called someEventHappened and then assigned onEvent
Related
I'm trying to test my coordinator flow but the child coordinator deinit called before the unit test case finished
My coordinator class
public final class AppCoordinator: Coordinator {
public var childCoordinators: [Coordinator] = []
public var navigationController: UINavigationController
var window: UIWindow?
public init(window: UIWindow?) {
self.window = window
let secController = SecController()
self.navigationController = UINavigationController(rootViewController: secController)
secController.delegate = self
}
public func start() {
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
}
}
extension AppCoordinator: SecControllerDelegate, SignInControllerDelegate {
public func removeSingIn() {
self.childCoordinators.removeFirst()
}
public func showSignIn() {
let signInCoordinator = SignInCoordinator(navigationController: self.navigationController)
signInCoordinator.delegate = self
self.childCoordinators.append(signInCoordinator)
signInCoordinator.start()
}
}
Unit test class
class AppCoordinatorTests: XCTestCase {
var coordinator: AppCoordinator!
override func setUp() {
super.setUp()
coordinator = AppCoordinator(window: UIWindow())
}
override func tearDown() {
coordinator = nil
super.tearDown()
}
func testStartMethod() {
coordinator.start()
XCTAssertNotNil(coordinator.window?.rootViewController)
}
func testShowSignIn() {
coordinator.showSignIn()
XCTAssertFalse(coordinator.childCoordinators.isEmpty)
XCTAssertTrue(coordinator.navigationController.visibleViewController is SignInController)
}
}
when try to test testShowSignIn always failed because of the deinit call removeSingIn function
public class SignInController: UIViewController {
public weak var delegate: SignInControllerDelegate?
public init() {
super.init(nibName: nil, bundle: nil)
}
deinit {
self.delegate?.removeSingIn()
}
}
Let's review the steps:
testShowSignIn calls coordinator.showSignIn(), where coordinator is an AppCoordinator.
showSignIn() instantiates a SignInCoordinator, and sets its delegate to the AppCoordinator instance.
Now we reach the important part:
We reach the end of showSignIn(). The SignInCoordinator goes out of scope, so Swift destroys it.
Nothing maintains a reference to the SignInCoordinator. But you want to test the interaction between the AppCoordinator and the SignInCoordinator. The code is fighting you, because AppCoordinator decides to create and destroy the SignInCoordinator on its own.
You can test it by changing the design. You have a couple of options.
Option 1: Change AppCoordinator to have a lazy computed property that returns the SignInCoordinator. This can work if you're okay with that design. Then the SignInCoordinator will continue to live, so that the test can query it. This improves the testability of AppCoordinator by exposing the SignInCoordinator.
Option 2: Have the test create a SignInCoordinator and pass it in as an argument to showSignIn(). Then the SignInCoordinator lifecycle will be managed completely outside of AppCoordinator.
I am trying to pass data receive from a network call to another view controller when user has clicked on a button. When making printing on the FirstVC, data is in, but when printing the result in the SecondVC, there is no more value. I don' t want to use delegate but closure instead.
Also, when trying to retain the memory cycle, an error appear...
class APIsRuler {
static var oneRecipeFound: ((OneRecipeSearch) -> ())?
}
class FirstVC: UIViewController {
func cellIsClicked(index: Int) {
APIsRuler.shared.getRecipe(from: recipeID) { (success, oneRecipe) in
if success, let oneRecipe = oneRecipe {
APIsRuler.oneRecipeFound?(oneRecipe)
self.performSegue(withIdentifier: "goToSecondVC", sender: self)
}
}
}
}
Class SecondVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
APIsRuler.oneRecipeFound = { result in
print(result)
}
}
}
Doing this in SecondVC
APIsRuler.oneRecipeFound = { result in
print(result)
}
and this in first
APIsRuler.oneRecipeFound?(oneRecipe)
have no inner communications , you need to read your data directly from the shared class in the secondVc after the segue or send it in
self.performSegue(withIdentifier: "goToSecondVC", sender: <#Herererere#>)
and implement prepareForSegue
Let’s think about the order in which things are happening:
class APIsRuler {
static var oneRecipeFound: ((OneRecipeSearch) -> ())? // 1
}
class FirstVC: UIViewController {
func cellIsClicked(index: Int) {
APIsRuler.shared.getRecipe(from: recipeID) { (success, oneRecipe) in
if success, let oneRecipe = oneRecipe {
APIsRuler.oneRecipeFound?(oneRecipe) // 2
self.performSegue(withIdentifier: "goToSecondVC", sender: self)
}
}
}
}
Class SecondVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
APIsRuler.oneRecipeFound = { result in // 3
print(result)
}
}
}
oneRecipeFound starts out life empty: it is nil.
In FirstVC, the cell is clicked. We call oneRecipeFound. It is still nil, so nothing happens.
In SecondVC, we set the value of oneRecipeFound. Now it has a value, but the call has already happened.
So unless you have a time machine in your pocket, so that you can reverse that order of events somehow, the strategy you’ve outlined is doomed to failure. Of course, if you call oneRecipeFound after setting it, it will work. For example:
Class SecondVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
APIsRuler.oneRecipeFound = { result in
print(result)
}
APIsRuler.oneRecipeFound?(oneRecipe) // prints
}
}
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) }()
I am working on two views that are subclassing subclass of UITableViewCell. In the base one (subclass of UITableViewCell) I am trying to setup gesture recognizer in a way that each of super class could change the behavior (eventually call didTapped method on it's delegate) of the tap.
I have written following code. I can use #selector(tap), however I think that using a variable instead of overriding a tap method in each super class is a much cleaner way. Is it even possible to use something like #selector(tapFunc)? If no what would be the cleanest and best from engineering point of view solution?
class BaseCell: UITableViewCell {
#objc var tapFunc: () -> () = { () in
print("Tapped")
}
#objc func tap() {
print("TEST")
}
func setupBasicViews(withContent: () -> ()) {
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(tapFunc))
contentView.isUserInteractionEnabled = true
contentView.addGestureRecognizer(tapGestureRecoginzer)
}
}
And then two views that are building on top of this one:
class ViewA: BaseCell {
//don't want to do this
override func tap() {
//do stuff
}
func setup {
//setup everything else
}
class ViewB: BaseCell {
var delegate: ViewBProtocool?
func setup {
tapFunc = { () in
delegate?.didTapped(self)
}
//setup everything else
}
You're not too far off. Make the following changes:
class BaseCell: UITableViewCell {
var tapFunc: (() -> Void)? = nil
// Called by tap gesture
#objc func tap() {
tapFunc?()
}
func setupBasicViews(withContent: () -> ()) {
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(tap))
contentView.isUserInteractionEnabled = true
contentView.addGestureRecognizer(tapGestureRecoginzer)
}
}
class ViewA: BaseCell {
func setup() {
//setup everything else
}
}
class ViewB: BaseCell {
var delegate: ViewBProtocol?
func setup() {
tapFunc = {
delegate?.didTapped(self)
}
//setup everything else
}
}
Now each subclass can optionally provide a closure for the tapFunc property.
I show above that tapFunc is optional with no default functionality in the base class. Feel free to change that to provide some default functionality if desired.
This is what I have in my QuickAddViewController.swift file
let exercisesData = ExerciseDatabase()
var workoutTypesDictionary = Dictionary<String,Dictionary<String,Array<String>>>()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
workoutTypesDictionary = self.exercisesData.exercisesByWorkoutType
}
func getWorkoutTypes() -> [String] {
var workoutTypesArray : [String] = []
for workoutType in workoutTypesDictionary.keys {
workoutTypesArray.append(workoutType)
}
return workoutTypesArray
}
This is my QuickAddViewTest.swift file
class QuickAddViewTests: XCTestCase {
var quickAddViewController : QuickAddViewController!
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
quickAddViewController = QuickAddViewController()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testGetWorkoutTypes() {
let workoutTypesArray = quickAddViewController.getWorkoutTypes()
let expectedArray = ["Weight Training", "Sports & Recreation", "Body Weight", "Cardio"]
print("Workout Array: \(workoutTypesArray)")
print("Expected Array: \(expectedArray)")
XCTAssertEqual(workoutTypesArray, expectedArray)
}
When I run the app and print getWorkoutTypes(), the function return the correct values. However, when I try to return the same values in testGetWorkoutTypes(), nothing gets returned and my test fails.
Add
quickAddViewController.loadViewIfNeeded()
This causes wires up storyboard connections (which makes other testing possible) and triggers a call back to viewDidLoad().