Swift - Update multiple viewControllers - swift

I would like to update several views, at the same time.
In my app, you can change your name. When the user change its name, would like to update 4 ViewControllers, to show the new name.
I use to use delegates to update one ViewController, but given that I would like to update more than 1, I don't think that's possible. (Maybe I'm wrong?)
I've heard about NotificationCenter, but I don't know if it's the best way to achieve that.
Something like that:
NotificationCenter.default.addObserver(self, selector: #selector(self.reload(_:)), name: NSNotification.Name(rawValue: "uploaded"), object: nil)
Is it good to do that?
What do you think? How would you do this?

You have many ways to do that, this depends on how your project struct.
If all views are in the same ViewController you can just listen some delegate and update manually all views, like, self.view1.updateName(newName), self.view2.updateName(newName)
else if you have many ViewControllers you can do something like Singleton and get the name when viewWillAppear, (I prefer don't use Singleton)
Example
class MyInfo{
public static let shared = MyInfo();
private(set) var name:String?
private init(){}
func update(name:String){
self.name = name;
}
}
And on your viewWillAppear you can do something like this
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated);
self.label.text = MyInfo.shared.name;
}
I like so much flux pattern, with ReSwift, but I think it can be very complex for a small problem in your case.
I hope was helpful

Related

Passing Data Indirectly to an Unrelated NSViewController

I am new to macOS Development and I am working on a project for macOS using Xcode 10 and Swift 4.2.
The project contains mainly 3 view controllers.
Firstly, ViewController (the main welcome screen which separates the other two) has two buttons to call the other two respectively.
Secondly, MakeEntry View Controller creates an array of strings data variable using a form type structure comprised of text views and save button etc. which in the end just saves all input data into an array of strings data variable called carrierArray
Thirdly, there is a split view controller for displaying two children view controller namely EntryList and EntryDetail
EntryList (the left pane) contains a Table View to display titles of entries and EntryDetail (the right pane) will contain the description of the title entry (somewhat like the default notes app of macOS)
I want to achieve a simple functionality of being able to access or read that Array of strings variable called carrierArray which is created when the MakeEntry view controller saves it into a global variable defined within its own class file But I want to access that array of strings anywhere and anytime later.
I cannot use delegates and protocols, closures, segues or storyboard identifiers to carry that data because I am not navigating to the Split View Controller straightaway and also because I want to store that data
to manipulate it further before displaying it in the right pane of split view controller (EntryDetail) .
I am unable to figure out whether how it might be possible to achieve this functionality using NSUserDefaults or CoreData.
Therefore I tried using the Notification Centre after storing that array of Strings in a Dictionary namely notifDictionary containing a key called carryData to be stored as the data object of notification centre And with some research and some trials and errors but without any luck all resulting in failure to get that data in the split view controller left pane class file namely (EntryDetail).
Code Snippets are as below, thanks a lot in advance for the kind help.
In MakeEntry View controller:
notifDictionary = ["carryData": carrierArray]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "dataCarrier") , object: nil, userInfo: notifDictionary)
In EntryList View Controller:
(Tried using both types of selector methods one at a time and even using them together but all without luck! Please Help!)
The Variable datumDict and datumArray and nothing but copy receivers for carrierArray and notifDictionary
var datumDict: [String:[String]] = [:]
var datumArray: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(self.getThatDict(_:)), name: NSNotification.Name(rawValue: "dataCarrier") , object: nil)
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "dataCarrier") , object: nil, queue: nil, using: catchNotification)
//datumArray = datumDict["carryData"]!
}
#objc func onNotification(notification:Notification)
{
print(notification.userInfo!)
}
func catchNotification(notification: Notification) -> Void
{
let theDict = notification.object as! NSDictionary
datumDict = theDict as! [String: [String]]
guard let theData = notification.userInfo!["carryData"] as? [String:[String]] else { return }
datumDict = theData
}
#objc func getThatDict(_ notification: NSNotification)
{
print(notification.userInfo ?? "")
if let dict = notification.userInfo as NSDictionary?
{
if let thatDict = dict["carryData"] as? [String: [String]]
{
datumDict = thatDict
}
}
}
With the caveat that "globals and singletons should be avoided," it sounds like they are a good fit for what you're trying to do. As you get more comfortable in Cocoa you can move into more sophisticated means of accomplishing this (dependency injection). Look into this as you get more comfortable with Cocoa.
Create a simple singleton type:
// AppState.swift
class AppState {
// Basic singleton setup:
static let shared = AppState()
private init() {} // private prevents instantiating it elsewhere
// Shared state:
var carrierArray: [String] = []
}
Access it from your view controllers:
// YourViewController.swift:
#IBAction func doSomething(_ sender: Any) {
AppState.shared.carrierArray = ...
}
If you need to update the other view controllers when this shared state changes, notifications are a good tool for that. You could do this with a didSet on carrierArray, or simply trigger the notification manually.

WatchOS: Pass data from one controller to another through "Next Page" segue

I am loading 3 interfacecontrollers with WKInterfaceController.reloadRootControllers with contexts in my app.
Everything works just fine. However, I need to pass data from Page 1 to Page 2 on swipe. It seems there is not segue that I can use programmatically to create a new context or pass data another way.
How could I solve this?
I don't want to create a singleton just for this. I know how to pass data in contexts with other segues, this question specifically relate to "Next Page" navigation on Watchkit. I couldn't find an answer so far.
Thanks!
Markus
Have you tried using contextForSegue in your first page?
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {
let myDate = "today"
return myDate
}
I'm been looking around for an answer for this question also..
NextPage Segue is similar to UIPageViewController in iOS. I have tried contextForSegue(withIdentifier segueIdentifier: String) but it is not getting called on swipe, and you can't give this segue an identifier in the storyboard.
The only way around it for me was to use NotificationCenter
In the first InterfaceController I post a notification with the object on willDisappear/didDeactivate doesn't really make a difference with watchOS and on the second InterfaceController I subscribed to it.
NotificationCenter.default.post(name: Notification.Name("interface1WillDisappear"), object: context)
NotificationCenter.default.addObserver(self, selector: #selector(awake(withNotification:)), name: Notification.Name("interface1WillDisappear"), object: nil)
Add I added this func
func awake(withNotification notification: Notification) {
guard let context = context as? MyContextObject else { return }
print(context)
}
Finally don't forget to remove the observer
deinit {
NotificationCenter.default.removeObserver(self)
}
Hope this helps!

Way to check data modification across many viewcontroller without using global variables?

I have an app which contains several viewControllers. On the viewDidAppear() of the first VC I call a set of functions which populate some arrays with information pulled from a database and then reload table data for a tableView. The functions all work perfectly fine and the desired result is achieved every time. What I am concerned about is how often viewDidAppear() is called. I do not think (unless I am wrong) it is a good idea for the refreshing functions to be automatically called and reload all of the data every time the view appears. I cannot put it into the viewDidLoad() because the tableView is part of a tab bar and if there are some modifications done to the data in any of the other tabs, the viewDidLoad() will not be called when tabbing back over and it would need to reload at this point (as modifications were made). I thought to use a set of variables to check if any modifications were done to the data from any of the other viewControllers to then conditionally tell the VDA to run or not. Generally:
override func viewDidAppear(_ animated: Bool) {
if condition {
//run functions
} else{
//don't run functions
}
}
The issue with this is that the data can be modified from many different viewControllers which may not segue back to the one of interest for the viewDidAppear() (so using a prepareForSegue wouldn't work necessarily). What is the best way to 'check' if the data has been modified. Again, I figured a set of bool variables would work well, but I want to stay away from using too many global variables. Any ideas?
Notification Center
struct NotificationName {
static let MyNotificationName = "kMyNotificationName"
}
class First {
init() {
NotificationCenter.default.addObserver(self, selector: #selector(self.notificationReceived), name: NotificationName.MyNotificationName, object: nil)
}
func notificationReceived() {
// Refresh table view here
}
}
class Second {
func postNotification() {
NotificationCenter.default.post(name: NotificationName.MyNotificationName, object: nil)
}
}
Once postNotification is called, the function notificationReceived in class First will be called.
Create a common global data store and let all the view controllers get their data from there. This is essentially a global singleton with some accompanying functions. I know you wanted to do this without global variables but I think you should consider this.
Create a class to contain the data. Also let it be able to reload the data.
class MyData {
static let shared = MyData()
var data : SomeDataType
func loadData() {
// Load the data
}
}
Register to receive the notification as follows:
static let dataChangedNotification = Notification.Name("DataChanged")
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidLoad() {
super.viewDidLoad()
// Establish a way for call activity to notify this class so it can update accordingly
NotificationCenter.default.addObserver(self, selector: #selector(handleDataChangedNotification(notification:)), name: "DataChanged", object: nil)
}
func handleDataChangedNotification(notification: NSNotification) {
// This ViewController was notified that data was changed
// Do something
}
func getDataToDisplay() {
let currentData = MyData.shared.data
// do something
}
// Any view controller would call this function if it changes the data
func sendDataChangeNotification() {
let obj = [String]() // make some obj to send. Pass whatever custom data you need to send
NotificationCenter.default.post(name: type(of: self).dataChangedNotification, object: obj)
}

Realm, best approach to save\update\Observe String to Realm Object

(looking for the best approach to save Realm property.
I have a UIViewController with a lot of TextView, etc that I fill from a Realm object.
Each time the textfield are modified, I need to send back to change un the realm property.
The (not cool) thing, are that I cannot save directly, I have to open a write transaction.
object.propertyA= “hello” // crash
try! realm.write { //work
userBeer?.Name = lblbeerName.text!
}
So, i found a bit painfull (and not clean) to to that for all text.
I’ve looked at rxRealm, but cannot see any (newbies) sample to make that.
So, I have 2 approach un mind
Modify the model getters and setters for the property
var beerName: String? {
get {
return self.Name
}
set {
try! realm.write {
self.txtName=beerName!
}
}
use the RXSwift approach from here (https://www.raywenderlich.com/149753/bond-tutorial-bindings-swift)
Bing the TextField.text to a var String, and observe this string to write.
What do you think?
My perfect world will be to find a way to bing the TextField.text property directly, something like:
myRealmObject.property.BindTo(self.txtName)
Katsumi from Realm here. Although it is not the best approach, I propose another way. You can use realm.beginWrite() and try! realm.commitWrite() instead of block-based API for a long-lived transaction.
For example, you can open a transaction when the view appeared, and then close the transaction when the view disappeared, like the following:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
realm.beginWrite()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
try! realm.commitWrite()
}
In this way, you can assign a value to the Realm object at any time in the view.
Be careful not to leave the transaction open. To avoid increasing file size, do not update data frequently in the background while transactions are open.
Change the property within the write block:
try! realm.write {
object.propertyA = “hello”
userBeer?.Name = lblbeerName.text!
}

Passing Data with Delegates

Im having a hard time understanding data passing with delegates, I have looked but I get confused by the answers as I'm usually trying to do the opposite of what needs to be done for mine.
What I am trying to do is pass a PFObject from my main viewController (a UITableView) to a PopOverViewController.
I have done this successfully, but I need to pass the object. What would be the best way to pass from the mainController (SOITableViewController) to the popover (DetailPopViewController)?
Where should the protocol go? Where should the Delegate method be placed, etc.
Thank you!
PrepareForSegue, NSUserDefault and Singleton
You have a few possible options to pass your data to other views depending how you want that data to be handled, I will explain each for you and you can choose which one best fit your need.
prepareForSegue: Method
I recommend this method if you want to hold your data for 1 segue transition, it's a good cause to pass this again to another view afterward you need to create another prepareForSegue within the new view. here is an example on how to do this:
First, you create 2 variables in both views, 1 to send (currentViewController.swift) and 1 to receive (toViewyourGoingController.swift).
currentViewController.swift var dataToSend: AnyObject?
ViewYourGoingController.swift var dataToReceive: AnyObject?
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
//Check your segue, this way you can transfer different data to different view. also make sure the identifier match your segue.
if segue.identifier == "toViewYourGoing" {
//Initial your second view data control
let ExchangeViewData = segue.destinationViewController as! toViewyourGoingController
//Send your data with segue
ExchangeViewData.dataToReceive = dataToSend
}
}
NSUserDefault
Now this method is good if you want to keep your data live as long as the app is installed, once the app is removed this will reset automatically. You also have the option to update the value of the key if you wish, here is how you do NSUserDefault:
I always like to register my NSUserDeafult to default setting, a lot of people just continue with the second step without registering.
Register NSUserDefault in AppDelgate.swift
NSUserDefaults.standardUserDefaults().registerDefaults(["valueName": AnyObject])
Set Value to your NSUserDefault, this depends on what type of data you're storing, should match the one with your registration if you did register. (Example of Boolean data type below)
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "valueName") //Bool Data Type
Make sure you synchronize once you set the value to the NSUserDefault, this way it will update instantly, otherwise it will update when it get a chance.
NSUserDefaults.standardUserDefaults().synchronize()
Receive Value: this will receive boolean value since we set boolean and register boolean.
let Variable: Bool! = NSUserDefaults.standardUserDefaults().boolForKey("valueName")
Singleton
Now singleton is basically a global variable that you can use them in any views, but some developers experience some bugs and difficulties, use it at your own risk, I recommend this method when you're definite that you will use that data a lot (STILL RISKY), but this method is like goddess of data handling :).
Create a NSObject subclass and call it DataManager.swift (I call it data manager cause it handle data.) as following:
import UIKit
class DataManager: NSObject {
//Store Data Globally
static var someData: Boo! //This Boolean, you can choose whatever you want.
}
the static is what keep your data live.
Now you can store and receive someData from anywhere like you handle any data type like this.
//Store
DataManager.someData = true
//Receive
print(DataManager.someData)
Challenges:
You can also use
Keychain
Sergey Kargopolov will walk you through how to use a third party to use swift keychain. Otherwise, you can take even harder challenge and create one yourself :P .
Key-Value Data in iCloud
The best way to do this would be to pass it over when you do the prepare for segue method. So to do this make a variable in your detailPopViewController. In this case your pop over segue in storyboard will have the segue identifier detailView. Also tblSearchResults is your tableView outlet (you can name it whatever you want). Is that what you were looking for?
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "detailView" {
if let detailViewController = segue.destinationViewController as? PopOverViewController{
if let indexPath = self.tblSearchResults.indexPathForCell(sender as! SOITTableViewController) {
detailViewController.objectInSecondVC = objectFromFirstVC[indexPath.row]
}
}
}
}