Update the viewController from the controller in MVC with SWIFT - swift

I'm creating an app with swift. The app get weather from openweathermap.com api in the class WeatherDataModel then when the data are loaded, the model ask the viewController to update the datas
I'm on Xcode 10.2.1 with swift 5
I've create a protocol called in the model to update the data but the updateDisplayDelegate?.updateWeatherDataOnDisplay() is always nil and even if I get the data from the JSON in the console it won't update on the screen
class WeatherDataModel {
var updateDisplayDelegate: ProtocolUpdateDisplay?
func updateWeaterData(json : JSON) {
updateDisplayDelegate?.updateWeatherDataOnDisplay()
}
}
public protocol ProtocolUpdateDisplay {
func updateWeatherDataOnDisplay()
}
class MainViewController: UIViewController {
let weatherDataModel = WeatherDataModel()
override func viewDidLoad() {
super.viewDidLoad()
weatherDataModel.updateDisplayDelegate = self
}
extension MainViewController: ProtocolUpdateDisplay {
func updateWeatherDataOnDisplay() {
cityLabel.text = weatherDataModel.city
tempLabel.text = weatherDataModel.temperature
weatherIcon.image = UIImage(named: weatherDataModel.weatherIconName)
}
}

You should not use delegation pattern for model. Consider using notification:
func updateWeaterData(json : JSON) {
NotificationCenter.default.post(Notification(name: Notification.Name("WeatherDidUpdate")))
}
and observe in any controller you want to respond to this notification:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(updateWeatherDataOnDisplay(_:)), name: Notification.Name("WeatherDidUpdate"), object: nil)
}
#objc func updateWeatherDataOnDisplay(_ notification: Notification) {
cityLabel.text = weatherDataModel.city
tempLabel.text = weatherDataModel.temperature
weatherIcon.image = UIImage(named: weatherDataModel.weatherIconName)
}
and remove observer at last:
deinit {
NotificationCenter.default.removeObserver(self)
}

Related

Why is my data not passing between View Controllers using closure?

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
}
}

What's the best way to watch the change of data in Cocoa

I have a singleton to store some global data for my macOS app, one of my ViewController keeps modifying data. I want to simultaneously show the changes in a View, which is related to another ViewController. what 's the best way to do this?
Global Data:
final class AppData {
static var logs: [LogData] = []
}
ViewController 1:
class FirstViewController: NSViewController {
AppData.logs.append(newLogData)
}
ViewController 2:
class SecondViewController: NSViewController {
// what's the best way to simultaneously watch the change of AppData.logs?
}
If your App is planned to be macOS only you can use a NSObjectController. This is definitively the easiest approach and you can do most of the configuration in Interface builder. It works internally with bindings. In case of an array you want to observe, you would use a NSArrayController.
One way is to use the notificationcenter
In viewcontroller2 add:
override func viewDidLoad() {
super.viewDidLoad()
notificationCenter.default.addObserver(self,
selector: #selector(view1DidChange),
name: "view1DidChange",
object: nil
)
}
#objc private func view1DidChange(_ notification: Notification) {
// Do something
}
In viewcontroller1 add
notificationCenter.default.post(name: "view1DidChange", object: self)
This can be repeated in every class, that should listen.
Here i am sharing the Delegate & Protocol approach to achieve this functionality.
final class AppData {
static var logs: [LogData] = []
}
protocol FirstViewControllerDelegate {
func ViewControllerDelegate(appData:[LogData])
}
class FirstViewController: NSViewController {
var delegate:FirstViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
AppData.logs.append(newLogData)
self. delegate?.ViewControllerDelegate(appData: AppData.logs)
}
}
class SecondViewController: NSViewController,FirstViewControllerDelegate {
var firstViewController:FirstViewController = FirstViewController()
override func viewDidLoad() {
self.firstViewController.delegate = self
}
func ViewControllerDelegate(appData:[LogData]){
//Do Update the UI
}
}

Why delegate method is not called?

I am trying to notify ChatViewController that a chat was deleted in MessagesViewController using a protocol, but the delegate method implemented in ChatViewController is never called.
In the navigationController hierarchy ChatViewController is on top of MessagesViewController.
protocol MessagesViewControllerDelegate:class {
func chatWasDeletedFromDatabase(chatUID: String)
}
class MessagesViewController: UITableViewController {
weak var delegate: MessagesViewControllerDelegate?
func observeChatRemoved() {
print("it is gonna be called")
//inform ChatViewController that a chat was deleted.
self.delegate?.chatWasDeletedFromDatabase(chatUID: chat.chatUID)
print("was called here") //prints as expected
}
}
class ChatViewController: JSQMessagesViewController {
var messagesVC: MessagesViewController?
override func viewDidLoad() {
super.viewDidLoad()
messagesVC = storyboard?.instantiateViewController(withIdentifier: "MessagesViewController") as! MessagesViewController
messagesVC?.delegate = self
}
}
extension ChatViewController: MessagesViewControllerDelegate {
func chatWasDeletedFromDatabase(chatUID: String) {
print("chatWasDeletedFromDatabase called") //never prints out
if self.chatSelected.chatUID == chatUID {
//popToRootViewController
}
}
It seems
weak var delegate: MessagesViewControllerDelegate?
is nil you have to set it to the ChatViewController presented instance what ever how you present it
let chat = ///
self.delegate = chat
self.navigationController?.pushViewController(chat,animated:true)
Also do
chat.messagesVC = self
as this
messagesVC = storyboard?.instantiateViewController(withIdentifier: "MessagesViewController") as! MessagesViewController
messagesVC?.delegate = self
isn't the currently presented messagesVC , so comment the above 2 lines

Protocol extension on an ObjC protocol

I have an Objective-C protocol which is used by mostly objective-C objects and one or two Swift objects.
I would like to extend the protocol in Swift and add 2 functions. One to register for a notification and another to handle the notification.
If I add these
func registerForPresetLoadedNotification() {
NSNotificationCenter.defaultCenter().addObserver(self as AnyObject,
selector: #selector(presetLoaded(_:)),
name: kPresetLoadedNotificationName,
object: nil)
}
func presetLoaded(notification: NSNotification) {
}
I get an error on the #selector which says:
Argument of '#selector' refers to a method that is not exposed to Objective-C
If I then mark presetLoaded as #objc I get an error which says:
#objc can only be used with members of classes, #objc protocols, and concrete extensions of classes
I also cannot mark the protocol extension as #objc
When I create the Objective-C protocol as a Swift protocol I get the same error.
Is there a way to achieve this that will work for Objective-C and Swift classes that use the protocol?
Indeed, you can't really mark a function of a protocol extension as #objc (or dynamic, which is equivalent by the way). Only methods of a class are allowed to be dispatched by Objective-C runtime.
In your particular case, if you really want to make it through protocol extension, I can propose the following solution (assuming your original protocol is named ObjcProtocol).
Let's make a wrapper for our notification handler:
final class InternalNotificationHandler {
private let source: ObjcProtocol
init(source: ObjcProtocol) {
// We require source object in case we need access some properties etc.
self.source = source
}
#objc func presetLoaded(notification: NSNotification) {
// Your notification logic here
}
}
Now we need extend our ObjcProtocol to introduce required logic
import Foundation
import ObjectiveC
internal var NotificationAssociatedObjectHandle: UInt8 = 0
extension ObjcProtocol {
// This stored variable represent a "singleton" concept
// But since protocol extension can only have stored properties we save it via Objective-C runtime
private var notificationHandler: InternalNotificationHandler {
// Try to an get associated instance of our handler
guard let associatedObj = objc_getAssociatedObject(self, &NotificationAssociatedObjectHandle)
as? InternalNotificationHandler else {
// If we do not have any associated create and store it
let newAssociatedObj = InternalNotificationHandler(source: self)
objc_setAssociatedObject(self,
&NotificationAssociatedObjectHandle,
newAssociatedObj,
objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return newAssociatedObj
}
return associatedObj
}
func registerForPresetLoadedNotification() {
NSNotificationCenter.defaultCenter().addObserver(self,
selector: #selector(notificationHandler.presetLoaded(_:)),
name: kPresetLoadedNotificationName,
object: self)
}
func unregisterForPresetLoadedNotification() {
// Clear notification observer and associated objects
NSNotificationCenter.defaultCenter().removeObserver(self,
name: kPresetLoadedNotificationName,
object: self)
objc_removeAssociatedObjects(self)
}
}
I know this might look not so elegant, so I'd really consider changing a core approach.
One note: You do might want to restrict your protocol extension
extension ObjcProtocol where Self: SomeProtocolOrClass
I found a way to do it :) Just avoid #objc all together :D
//Adjusts UITableView content height when keyboard show/hide
public protocol KeyboardObservable: NSObjectProtocol {
func registerForKeyboardEvents()
func unregisterForKeyboardEvents()
}
extension KeyboardObservable where Self: UITableView {
public func registerForKeyboardEvents() {
let defaultCenter = NotificationCenter.default
var tokenShow: NSObjectProtocol!
tokenShow = defaultCenter.addObserver(forName: .UIKeyboardDidShow, object: nil, queue: nil) { [weak self] (notification) in
guard self != nil else {
defaultCenter.removeObserver(tokenShow)
return
}
self!.keyboardWilShow(notification as NSNotification)
}
var tokenHide: NSObjectProtocol!
tokenHide = defaultCenter.addObserver(forName: .UIKeyboardWillHide, object: nil, queue: nil) { [weak self] (notification) in
guard self != nil else {
defaultCenter.removeObserver(tokenHide)
return
}
self!.keyboardWilHide(notification as NSNotification)
}
}
private func keyboardDidShow(_ notification: Notification) {
let rect = ((notification as NSNotification).userInfo![UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
let height = rect.height
var insets = UIEdgeInsetsMake(0, 0, height, 0)
insets.top = contentInset.top
contentInset = insets
scrollIndicatorInsets = insets
}
private func keyboardWillHide(_ notification: Notification) {
var insets = UIEdgeInsetsMake(0, 0, 0, 0)
insets.top = contentInset.top
UIView.animate(withDuration: 0.3) {
self.contentInset = insets
self.scrollIndicatorInsets = insets
}
}
public func unregisterForKeyboardEvents() {
NotificationCenter.default.removeObserver(self)
}
}
Example
class CreateStudentTableView: UITableView, KeyboardObservable {
init(frame: CGRect, style: UITableViewStyle) {
super.init(frame: frame, style: style)
registerForKeyboardEvents()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Swift function textfield got focus OSX

Currently I am having multiple textfields in a view. If the user taps at one of them there should be a function responding to the event. Is there a way on how to do react (if a textfield got the focus)? I tried it with the NSTextFieldDelegate method but there is no appropriate function for this event.
This is how my code looks at the moment:
class ViewController: NSViewController, NSTextFieldDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let textField = NSTextField(frame: CGRectMake(10, 10, 37, 17))
textField.stringValue = "Label"
textField.bordered = false
textField.backgroundColor = NSColor.controlColor()
view.addSubview(textField)
textField.delegate = self
let textField2 = NSTextField(frame: CGRectMake(30, 30, 37, 17))
textField2.stringValue = "Label"
textField2.bordered = false
textField2.backgroundColor = NSColor.controlColor()
view.addSubview(textField2)
textField2.delegate = self
}
func control(control: NSControl, textShouldBeginEditing fieldEditor: NSText) -> Bool {
print("working") // this only works if the user enters a charakter
return true
}
}
The textShouldBeginEditing function only handles the event if the user tries to enter a character but this isn't what I want. It has to handle the event if he clicks on the textfield.
Any ideas, thanks a lot?
Edit
func myAction(sender: NSView)
{
print("aktuell: \(sender)")
currentObject = sender
}
This is the function I want to call.
1) Create a subclass of NSTextField.
import Cocoa
class MyTextField: NSTextField {
override func mouseDown(theEvent:NSEvent) {
let viewController:ViewController = ViewController()
viewController.textFieldClicked()
}
}
2) With Interface building, select the text field you want to have a focus on. Navigate to Custom Class on the right pane. Then set the class of the text field to the one you have just created.
3) The following is an example for ViewController.
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
func textFieldClicked() -> Void {
print("You've clicked on me!")
}
}
4) Adding text fields programmatically...
import Cocoa
class ViewController: NSViewController {
let myField:MyTextField = MyTextField()
override func viewDidLoad() {
super.viewDidLoad()
//let myField:MyTextField = MyTextField()
myField.setFrameOrigin(NSMakePoint(20,70))
myField.setFrameSize(NSMakeSize(120,22))
let textField:NSTextField = NSTextField()
textField.setFrameOrigin(NSMakePoint(20,40))
textField.setFrameSize(NSMakeSize(120,22))
self.view.addSubview(myField)
self.view.addSubview(textField)
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
func textFieldClicked() -> Void {
print("You've clicked on me!")
}
}
I know it’s been answered some while ago but I did eventually find this solution for macOS in Swift 3 (it doesn’t work for Swift 4 unfortunately) which notifies when a textfield is clicked inside (and for each key stroke).
Add this delegate to your class:-
NSTextFieldDelegate
In viewDidLoad() add these:-
imputTextField.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: Notification.Name.NSTextViewDidChangeSelection, object: nil)
Then add this function:-
func textDidChange(_ notification: Notification) {
print("Its come here textDidChange")
guard (notification.object as? NSTextView) != nil else { return }
let numberOfCharatersInTextfield: Int = textFieldCell.accessibilityNumberOfCharacters()
print("numberOfCharatersInTextfield = \(numberOfCharatersInTextfield)")
}
Hope this helps others.