MVVM project. How to connect Model View to the View - swift

I am trying to make an app that will show the weather in my city. I am using MVVM architecture and I have my Model, ModelView and View as follows. I have a variable inside the WeatherModelView class that I want to use in my view controller:
label.text = "(weatherViewModel.res?.main.temp ?? -123)"
but it does not work.
(https://i.stack.imgur.com/8BTEJ.png)
View Controller](https://i.stack.imgur.com/qW54n.png)
It does not give an error, it simply prints -123.0 on the label, which is the nil case after unwrapping. I would like it to print the actual weather. I don't think there are problems with the URL or the JSON decoding.
This is what is wrongfully shown when I run it: simulator

In "viewdidload" "fetchWeather" is not complete.
You need set "label.text" after it completed
change res in your view model
var res = WeatherModel? {
didSet {
resHasData?()
}
}
var resHasData?: (() -> Void)?
add my code in the last line "viewDidLoad"
weatherViewModel.resHasData = { [weak sekf] in
guard let self = self else { return }
self.label.text = "\(self.weatherViewModel.res?.main.temp ?? -123)"
}
good luck.

Related

Swift launch view only when data received

I'm getting info from an API using the following function where I pass in a string of a word. Sometimes the word doesn't available in the API if it doesn't available I generate a new word and try that one.
The problem is because this is an asynchronous function when I launch the page where the value from the API appears it is sometimes empty because the function is still running in the background trying to generate a word that exists in the API.
How can I make sure the page launches only when the data been received from the api ?
static func wordDefin (word : String, completion: #escaping (_ def: String )->(String)) {
let wordEncoded = word.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let uri = URL(string:"https://dictapi.lexicala.com/search?source=global&language=he&morph=false&text=" + wordEncoded! )
if let unwrappedURL = uri {
var request = URLRequest(url: unwrappedURL);request.addValue("Basic bmV0YXlhbWluOk5ldGF5YW1pbjg5Kg==", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
do {
if let data = data {
let decoder = JSONDecoder()
let empty = try decoder.decode(Empty.self, from: data)
if (empty.results?.isEmpty)!{
print("oops looks like the word :" + word)
game.wordsList.removeAll(where: { ($0) == game.word })
game.floffWords.removeAll(where: { ($0) == game.word })
helper.newGame()
} else {
let definition = empty.results?[0].senses?[0].definition
_ = completion(definition ?? "test")
return
}
}
}
catch {
print("connection")
print(error)
}
}
dataTask.resume()
}
}
You can't stop a view controller from "launching" itself (except not to push/present/show it at all). Once you push/present/show it, its lifecycle cannot—and should not—be stopped. Therefore, it's your responsibility to load the appropriate UI for the "loading state", which may be a blank view controller with a loading spinner. You can do this however you want, including loading the full UI with .isHidden = true set for all view objects. The idea is to do as much pre-loading of the UI as possible while the database is working in the background so that when the data is ready, you can display the full UI with as little work as possible.
What I'd suggest is after you've loaded the UI in its "loading" configuration, download the data as the final step in your flow and use a completion handler to finish the task:
override func viewDidLoad() {
super.viewDidLoad()
loadData { (result) in
// load full UI
}
}
Your data method may look something like this:
private func loadData(completion: #escaping (_ result: Result) -> Void) {
...
}
EDIT
Consider creating a data manager that operates along the following lines. Because the data manager is a class (a reference type), when you pass it forward to other view controllers, they all point to the same instance of the manager. Therefore, changes that any of the view controllers make to it are seen by the other view controllers. That means when you push a new view controller and it's time to update a label, access it from the data property. And if it's not ready, wait for the data manager to notify the view controller when it is ready.
class GameDataManager {
// stores game properties
// updates game properties
// does all thing game data
var score = 0
var word: String?
}
class MainViewController: UIViewController {
let data = GameDataManager()
override func viewDidLoad() {
super.viewDidLoad()
// when you push to another view controller, point it to the data manager
let someVC = SomeOtherViewController()
someVC.data = data
}
}
class SomeOtherViewController: UIViewController {
var data: GameDataManager?
override func viewDidLoad() {
super.viewDidLoad()
if let word = data?.word {
print(word)
}
}
}
class AnyViewController: UIViewController {
var data: GameDataManager?
}

Multiple UITextFields and textDidChangeNotification notification

I was playing with Combine framework lately and was wondering if it is possible to create some smart extension to get text changes as Publisher.
Let's say I've got two UITextFields:
firstTextField.textPub.sink {
self.viewModel.first = $0
}
secondTextField.textPub.sink {
self.viewModel.second = $0
}
where first and second variable is just `#Published var first/second: String = ""
extension UITextField {
var textPub: AnyPublisher<String, Never> {
return NotificationCenter.default
.publisher(for: UITextField.textDidChangeNotification)
.map {
guard let textField = $0.object as? UITextField else { return "" }
return textField.text ?? ""
}
.eraseToAnyPublisher()
}
}
This doesn't work because I'm using shared instance of NotificationCenter so when I make any change to any of textFields it will propagate new value to both sink closures. Do you think is there any way to achieve something similar to rx.text available in RxSwift? I was thinking about using addTarget with closure but it would require using associated objects from Objective-C.
I figured this out. We can pass object using NotificationCenter and then filter all instances that are not matching our instance. It seems to work as I expected:
extension UITextField {
var textPublisher: AnyPublisher<String, Never> {
NotificationCenter.default
.publisher(for: UITextField.textDidChangeNotification, object: self)
.compactMap { $0.object as? UITextField }
.map { $0.text ?? "" }
.eraseToAnyPublisher()
}
}
I would suggest you add subscribers to the view modal, and connect them a text field publisher within the context of the view controller.
NotificationCenter is useful to dispatch events app-wide; there's no need to use it when connecting items that are fully owned by the View Controller. However, once you've updated the view modal it may make sense to publish a 'View Modal Did Change' event to NotificationCenter.

JSQMessagesviewcontroller can't perform segues

so I created a chat view controller using the JSQMessagesViewController following this tutorial here: https://learnappmaking.com/chat-app-ios-firebase-swift-xcode/#comment-1930 my code is more or less the same, I didn't tweak anything significant in it, the tutorial is only for a single view controller so I added another view controllers for the app but every time it perform segues, I get the error SIGABRT, no matter if I segues with performSegue or with the back button in navigation bar, it keeps giving signal SIGABRT. any help would be appreciated.
this is my viewdidload:
override func viewDidLoad() {
super.viewDidLoad()
senderId = "1111"
senderDisplayName = "Bob"
title = "Steve"
inputToolbar.contentView.leftBarButtonItem = nil
collectionView.collectionViewLayout.incomingAvatarViewSize = CGSize.zero
collectionView.collectionViewLayout.outgoingAvatarViewSize = CGSize.zero
let query = Constants.refs.databaseChats.queryLimited(toLast: 10)
_ = query.observe(.childAdded, with: { [weak self] snapshot in
if let data = snapshot.value as? [String: String],
let id = data["sender_id"],
let name = data["name"],
let text = data["text"],
!text.isEmpty
{
if let message = JSQMessage(senderId: id, displayName: name, text: text)
{
self?.messages.append(message)
self?.finishReceivingMessage()
}
}
})
// Do any additional setup after loading the view.
}
SIGABRT (signal abort) is typically from a referencing error in your storyboard. Did you ever change the name of a class or make a connection from a button of one view controller to another and then delete it? If you changed the name of a class you must make the sure the name in the code of the class matches that. If you deleted a button connection between view controllers, click on the controller itself and under the connections tab you must delete it.

Does this view controller leak in a "willSet/didSet" pair?

You have a vc (green) and it has a panel (yellow) "holder"
Say you have ten different view controllers...Prices, Sales, Stock, Trucks, Drivers, Palettes, which you are going to put in the yellow area, one at a time. It will dynamically load each VC from storyboard
instantiateViewController(withIdentifier: "PricesID") as! Prices
We will hold the current VC one in current. Here's code that will allow you to "swap between" them...
>>NOTE, THIS IS WRONG. DON'T USE THIS CODE<<
One has to do what Sulthan explains below.
var current: UIViewController? = nil {
willSet {
// recall that the property name ("current") means the "old" one in willSet
if (current != nil) {
current!.willMove(toParentViewController: nil)
current!.view.removeFromSuperview()
current!.removeFromParentViewController()
// "!! point X !!"
}
}
didSet {
// recall that the property name ("current") means the "new" one in didSet
if (current != nil) {
current!.willMove(toParentViewController: self)
holder.addSubview(current!.view)
current!.view.bindEdgesToSuperview()
current!.didMove(toParentViewController: self)
}
}
}
>>>>>>>>IMPORTANT!<<<<<<<<<
Also note, if you do something like this, it is ESSENTIAL to get rid of the yellow view controller when the green page is done. Otherwise current will retain it and the green page will never be released:
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
current = nil
super.dismiss(animated: flag, completion: completion)
}
Continuing, you'd use the current property like this:
func showPrices() {
current = s.instantiateViewController(withIdentifier: "PricesID") as! Prices
}
func showSales() {
current = s.instantiateViewController(withIdentifier: "SalesID") as! Sales
}
But consider this, notice "point X". Normally there you'd be sure to set the view controller you are getting rid of to nil.
blah this, blah that
blah.removeFromParentViewController()
blah = nil
However I (don't think) you can really set current to nil inside the "willSet" code block. And I appreciate it's just about to be set to something (in didSet). But it seems a bit strange. What's missing? Can you even do this sort of thing in a computed property?
Final usable version.....
Using Sulthan's approach, this then works perfectly after considerable testing.
So calling like this
// change yellow area to "Prices"
current = s.instantiateViewController(withIdentifier: "PricesID") as! Prices
// change yellow area to "Stock"
current = s.instantiateViewController(withIdentifier: "StickID") as! Stock
this works well...
var current: UIViewController? = nil { // ESSENTIAL to nil on dismiss
didSet {
guard current != oldValue else { return }
oldValue?.willMove(toParentViewController: nil)
if (current != nil) {
addChildViewController(current!)
holder.addSubview(current!.view)
current!.view.bindEdgesToSuperview()
}
oldValue?.view.removeFromSuperview()
oldValue?.removeFromParentViewController()
if (current != nil) {
current!.didMove(toParentViewController: self)
}
}
// courtesy http://stackoverflow.com/a/41900263/294884
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
// ESSENTIAL to nil on dismiss
current = nil
super.dismiss(animated: flag, completion: completion)
}
Let's divide the question into two: (1) Is there a "leak"? and (2) Is this a good idea?
First the "leak". Short answer: no. Even if you don't set current to nil, the view controller it holds obviously doesn't "leak"; when the containing view controller goes out of existence, so does the view controller pointed to by current.
The current view controller does, however, live longer than it needs to. For that reason, this seems a silly thing to do. There is no need for a strong reference current to the child view controller, because it is, after all, your childViewControllers[0] (if you do the child view controller "dance" correctly). You are thus merely duplicating, with your property, what the childViewControllers property already does.
So that brings us to the second question: is what you are doing a good idea? No. I see where you're coming from — you'd like to encapsulate the "dance" for child view controllers. But you are doing the dance incorrectly in any case; you're thus subverting the view controller hierarchy. To encapsulate the "dance", I would say you are much better off doing the dance correctly and supplying functions that perform it, along with a computed read-only property that refers to childViewController[0] if it exists.
Here, I assume we will only ever have one child view controller at a time; I think this does much better the thing you are trying to do:
var current : UIViewController? {
if self.childViewControllers.count > 0 {
return self.childViewControllers[0]
}
return nil
}
func removeChild() {
if let vc = self.current {
vc.willMove(toParentViewController: nil)
vc.view.removeFromSuperview()
vc.removeFromParentViewController()
}
}
func createChild(_ vc:UIViewController) {
self.removeChild() // There Can Be Only One
self.addChildViewController(vc) // *
// ... get vc's view into the interface ...
vc.didMove(toParentViewController: self)
}
I don't think that using didSet is actually wrong. However, the biggest problem is that you are trying to split the code between willSet and didSet because that's not needed at all. You can always use oldValue in didSet:
var current: UIViewController? = nil {
didSet {
guard current != oldValue else {
return
}
oldValue?.willMove(toParentViewController: nil)
if let current = current {
self.addChildViewController(current)
}
//... add current.view to the view hierarchy here...
oldValue?.view.removeFromSuperview()
oldValue?.removeFromParentViewController()
current?.didMove(toParentViewController: self)
}
}
By the way, the order in which the functions are called is important. Therefore I don't advise to split the functionality into remove and add. Otherwise the order of viewDidDisappear and viewDidAppear for both controllers can be surprising.

Using a UIViewController as the default value for an optional parameter, but I get the "X does not have a member named Y" error

So I have been having fun with default parameter values.
class containerViewController: UIViewController {
var detailView:UIViewController?
override func viewDidLoad(){
super.viewDidLoad()
detailView = anotherViewController()
}
func hideDetailView(vc:UIViewController? = detailView){ // <- THIS LINE
// code
}
}
The line Ive marked produces an error:
'containerViewController.Type' does not have a member named 'detailView'
Ive been reading online, including this question, but I cant seem to figure out how to fix this.
What I want is to be able to use hideDetailView() and if I send in a specific view controller as a parameter to that function, it hides that specific view controller. If I dont send any parameter, it just hides the current view controller that is held in the detailView parameter.
How can I achieve this?
You can use nil for the default value, and check if nil in the body.
func hideDetailView(vc:UIViewController? = nil){ // <- THIS LINE
let vc_ = vc ?? detailView
// code
}
But In this case, you can't distinguish following calls:
// passing `nil` as Optional<UIViewController>
let vc:UIViewController? = nil
container.hideDetailView(vc: vc)
// use default value
container.hideDetailView()
If you don't like that, you can use UIViewController??:
func hideDetailView(vc:UIViewController?? = nil){
let vc_ /*: UIViewController? */ = vc ?? detailView
// code
}