Howto pass NSManagedObject via protocol/delegate? - ios

I have a managed object defined:
#objc (Game)
class Game: NSManagedObject {
#NSManaged var player1: Player
#NSManaged var player2: Player
#NSManaged var totalScore: NSNumber
#NSManaged var gameDate: NSDate
}
I initialize it from ViewControllerA, then I give it to ViewControllerB using the delegate pattern. The protocol looks like this:
protocol gameProtocol {
func gameFunction(input: Game)
}
ViewControllerB signs up for the protocol:
class ViewControllerB: UIViewController, gameProtocol {...}
and implements this function to conform:
func gameFunction(input: Game) {
let currentGame = input
}
Then ViewControllerA can send a Game object to VCB as follows:
var gameDelegate: gameProtocol!
gameDelegate.gameFunction(myInitializedAndSavedGameObject)
This all works, but I need a class level variable inside ViewControllerB so that other code can be written to depend on the game. This, of course, does not work:
var currentGame = Game()
func gameFunction(input: Game) {
currentGame = input
}
I don't know the right words for it, but I think I want an initialized, empty Game object. I suppose I could write a convenience init that makes a temporary Game, but that doesn't seem like a good idea.
My current workaround is to have an NSManagedObjectID() and then recreate the object from the ID. But this is a lot of repeated code to get at an object that is central to what this ViewController is designed to work with.

So you want to push your NSManagedObject to your second View Controller?- You dont need an delegate, you could send your object as instance variable, as Wain already said.
For Example (in your MainViewController)
class MainViewControntroller: UIViewController {
var currentGameObject:YOURNSMANAGEDOBJECT!
func viewDidLoad() {
// load your Object
var fetchRequest = NSFetchRequest(entityName: "Game")
.....
games = context.executeFetchRequest(fetchRequest, error: nil) as [YOURNSMANAGEDOBJECT]
if(games.count > 0) {
// set to any game object (here is only the last)
currentGameObject = games.last?
}
}
}
// Initialize your Second View Controller (for example when using segues)
if(segue.identifier == "yourIdentifierInStoryboard") {
var yourNextViewController = (segue.destinationViewController as yourNextViewControllerClass)
yourNextViewController.currentGameObject = currentGameObject
So you are able to use your NSManagedObject in your SecondViewController - if you want to push it back, you could use a delegate.

'lazy var currentGame = Game()' seems to be what I want. By making the var lazy, the designated initializer is never incorrectly called. I am certain that the first thing to touch the var will be the gameFunction method, so my other code will compile and it won't crash at runtime. Alternative suggestions welcome.

Related

How to pass data to the final view controller

I am new to Swift and am building an app to learn. Right now I am making the registration section of the app.
I thought the UX would be better if there were multiple VC's asking a single question, i.e. one for your name, one for your birthdate, etc as opposed to jamming all that into a single view controller. The final view controller collects all of that information and sends a dictionary as FUser object to be saved on Firebase.
I figured I could instantiate the final view controller on each of the previous five view controllers and pass that data directly to the end. I kept getting errors and figured out that the variables were nil. It works just fine if I pass the data directly to the next view controller but it doesn't seem to let me send it several view controllers down. Obviously there's a nuance to how the memory is being managed here that I'm not tracking.
Is there a way to do what I am trying to do or do I have to pass the data through each view controller along the way?
import UIKit
class FirstViewController: UIViewController {
//MARK: - IBOutlets
#IBOutlet weak var firstNameTextField: UITextField!
//MARK: - ViewLifeCycle
override func viewDidLoad() {
super.viewDidLoad()
}
//MARK: - IBActions
#IBAction func continueToMiddleViewController(_ sender: Any) {
let vcFinal = storyboard?.instantiateViewController(withIdentifier:
"finalVC") as! finalViewController
vcFinal.firstName = firstNameTextField.text
let vc = storyboard?.instantiateViewController(withIdentifier:
"middleVC") as! middleViewController
vc.modalPresentationStyle = .fullScreen
present(vc, animated: false)
}
...
}
import UIKit
class FinalViewController: UIViewController {
var firstName: String?
...
//MARK: - ViewLifeCycle
override func viewDidLoad() {
super.viewDidLoad()
}
...
}
TL;DR: The fastest one that would solve your problem is creating a singleton
There are many strategies for this. For a starter, it might be a good idea to read some begginer articles, like this one. I can update this answer if you don't find it useful, but it'd look just like the article
Viewcontroller's variable can't be initiated until any of the init method is called.
There are detailed answers on this thread.
Passing Data between ViewControllers
Another way to approach this problem could be to make use of closures. Note that personally I've moved away from using storyboards but I'll try to explain still. Closures are also referred to as callbacks, blocks, or in some context like here - completions.
You can declare a closure like let onSubmitInfo: (String?) -> Void below, it stores a reference to a block of code that can be executed at a later stage just like a function and it takes an optional string as a parameter just like a function can.
The closures are specified in the initialisers where a block of code is passed into the respective classes below and the closures are then called in the IBActions that will trigger the block of code that is defined where the below classes are initialised:
class First: UIViewController {
// MARK: - IBOutlets
#IBOutlet weak var firstNameTextField: UITextField!
// MARK: - Properties
private let onSubmitInfo: (String?) -> Void
init(onSubmitInfo: (String?) -> Void) {
self.onSubmitInfo = onSubmitInfo
}
// MARK: - IBActions
#IBAction func continue(_ sender: Any) {
onSubmitInfo(firstNameTextField.text)
}
}
class Second: UIViewController {
// MARK: - IBOutlets
#IBOutlet weak var lastNameTextField: UITextField!
// MARK: - Properties
private let onSubmitInfo: (String?) -> Void
init(onSubmitInfo: (String?) -> Void) {
self.onSubmitInfo = onSubmitInfo
}
// MARK: - IBActions
#IBAction func continue(_ sender: Any) {
onSubmitInfo(lastNameTextField.text)
}
}
To manage showing the above views and collecting the values returned by their closures (i.e. onSubmitInfo) we create a FlowController class that will also show the next view when the closure is called.
In FlowController we define the closures or blocks of code to be executed when it is called inside the IBAction in the respective First and Second classes above.
The optional string that is provided in the respective First and Second classes is used as the (firstName) and (secondName) closure properties below:
class FlowController: UIViewController {
private var fistName: String?
private var lastName: String?
...
private func showFirstView() {
let firstViewController = First(onSubmitInfo: { (firstName) in
self.firstName = firstName
showSecondView()
})
navigationController?.pushViewController(
firstViewController,
animated: true)
}
private func showSecondView() {
let secondViewController = Second(onSubmitInfo: { (lastName) in
self.lastName = lastName
showFinalView()
})
navigationController?.pushViewController(
secondViewController,
animated: true)
}
private func showFinalView() {
let finalViewController = Final(
firstName: firstName,
lastName: lastName)
navigationController?.pushViewController(
finalViewController,
animated: true)
}
}
The FlowController finally shows the Final view controller after it has collected the firstName form the First view controller and the lastName form the Second view controller in the showFinalView function above.
class Final: UIViewController {
let firstName: String
let lastName: String
...
}
I hope this is a shove in the right direction. I have moved away from storyboards because I find creating views in code is more verbose and clear on peer reviews and it was also easier for me to manage constraints and just to manage views in general.

Create a member for the UIViewController class

I tried to do some research but couldn't figure it out, so is it possible to create a member for the class UIViewController, or any class for that matter?
In every single one of my UIViewController subclasses I declare the data member
userdata = [NSManagedObject]()
So I was wondering if I could declare the variable "userdata" inside the actual UIViewController class, either directly or through an external file.
You can simply create a sub-class of UIViewController which has the userdata property and then derive all of your view controllers from that class instead of UIViewController
Something like:
class BaseViewController:UIViewContrller {
var userdata = [NSManagedObject]()
}
class NewViewController:BaseViewController {
// Your sub view controller implementation goes here
}
You should use extensions.
extension UIViewController {
var userData : [NSManagedObject] {
get {
return [NSManagedObject]()
}
}
}
If you don't want every UIViewController to have that property, you will have to use subclassing.
class DataViewController:UIViewContrller {
var userdata = [NSManagedObject]()
}
class NewViewController:DataViewController {
// Do something stuff to the View here
}
You can use extensions if userData is a computed property:
extension UIViewController {
var userData: [NSManagedObject] {
get { return an array from somewhere else }
set { set the value to somewhere else }
}
}
If your property is not computed but stored, you must use a subclass:
class BaseViewController: UIViewController {
var userData: [NSManagedObject] = []
}
And make every VC of yours inherit this class. The disadvantage of using this approach is that your view controllers can't inherit any other class, like UITableViewController.
So here is the best method I came up with.
Create a protocol:
protocol MyVC {
var userData: [NSManagedObject] { get set }
}
Now make every VC of yours conform to this protocol. In every VC, just start typing userData and use enter to select the right completion that Xcode provides and the property will be automatically added for you. If you forgot to do this, the compilation will fail.

Access another view controller's property after an event

I have 2 ViewControllers, the first one has an array that is filled by a fetch request once the ViewController appears. The second ViewController has a reference to that array. However, that is referencing an empty array which before it's being filled.
I want to get a reference once the array is filled. Here is a snippet.
class VC1: UIViewController {
let coreDataObjects = [NSManagedObject]() //At this moment, this array is empty
func viewWillAppear {
fetchRequestThatFillsTheArray() //Here, the array is filled
}
fetchRequestThatFillsTheArray() {
//calling NSFetchRequest
}
}
class VC2: UIViewController {
let vc1 = VC1()
vc1.coreDataObjects //Empty array
}
Thanks.
Here is the solution:
class VC1: UIViewController {
lazy var coreDataObjects = fetchRequestThatFillsTheArray()
func fetchRequestThatFillsTheArray() {
//calling NSFetchRequest
}
}
If you still want a solution on your original code, or you have issue to use fetchRequestThatFillsTheArray() in lazy . The you have to use NSNotification. Read Notification Programming Topics
--- UPDate for your comment----
You should change you code a little.
class VC1: UIViewController {
lazy var coreDataObjects:[NSManagedObject] = self.fetchRequestThatFillsTheArray()
func fetchRequestThatFillsTheArray() -> [NSManagedObject] {
//calling NSFetchRequest
}
}
However, since you used self.property in your fetchRequestThatFillsTheArray(), you can't use this solution.
You have to use NSNotification. That means you send a notification when fetchRequestThatFillsTheArray() finished. And register the notification in vc2. When vc2 get the notification, it does what it wants.

Using prepareForSegue to pass data to ViewController later in app

I was wondering, when passing data using prepareForSegue, can you pass data to a View Controller later in the app? For example on the first ViewController I have the user enter their name. It's not until the very end, so a few views later, do I need to display their name. Is there a way to pass their name without having to go to the end view right away?
Use a Coordinator.
It's really easy to decouple your ViewControllers:
instead of using segues give every ViewController a delegate
create a coordinator object (this object knows your screen flow, not your screens)
the coordinator creates the ViewControllers (it can use UIStoryboard instantiateViewController(withIdentifier:) so ViewController A does not have to know that ViewController B exists
instead of calling performSegue you just call your delegate and pass in the data
Benefits
Simple to use
Easy to reorder screens in a flow
Highly decoupled (easier testing)
Very nice for A/B testing
Scales a lot (you can have multiple coordinators, one for each flow)
Sample
Let's say you have 3 VCs, the first one asks for your name, the second for your age and the third displays the data. It would make no sense that AgeViewController knew that NameViewController existed, later on you may want to change their order or even merge them.
Name View Controller
protocol NameViewControllerDelegate: class {
func didInput(name: String)
}
class NameViewController: UIViewController {
weak var delegate: NameViewControllerDelegate?
#IBOutlet var nameTextField: UITextField!
//Unimportant stuff ommited
#IBAction func submitName(sender: Any) {
guard let name = nameTextField.text else {
// Do something, it's up to you what
return
}
delegate?.didInput(name: name)
}
}
Age View Controller
protocol AgeViewControllerDelegate: class {
func didInput(age: Int)
}
class AgeViewController: UIViewController {
weak var delegate: AgeViewControllerDelegate?
#IBOutlet var ageTextField: UITextField!
//Unimportant stuff ommited
#IBAction func submitAge(sender: Any) {
guard let ageString = ageTextField.text,
let age = Int(ageString) else {
// Do something, it's up to you what
return
}
delegate?.didInput(age: age)
}
}
Displayer View Controller
class DisplayerViewController: UIViewController {
var age: Int?
var name: String?
}
Coordinator
class Coordinator {
var age: Int?
var name: String?
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
fileprivate lazy var storyboard: UIStoryboard = {
return UIStoryboard(name: "MyStoryboard", bundle: nil)
}()
//This works if you name your screns after their classes
fileprivate func viewController<T: UIViewController>(withType type: T.Type) -> T {
return storyboard.instantiateViewController(withIdentifier: String(describing: type(of: type))) as! T
}
func start() -> UIViewController {
let viewController = self.viewController(withType: NameViewController.self)
viewController.delegate = self
navigationController.viewControllers = [viewController]
return viewController
}
}
Coordinator + Name View Controller Delegate
extension Coordinator: NameViewControllerDelegate {
func didInput(name: String){
self.name = name
let viewController = self.viewController(withType: AgeViewController.self)
viewController.delegate = self
navigationController.pushViewController(viewController, animated: true)
}
}
Coordinator + Age View Controller Delegate
extension Coordinator: AgeViewControllerDelegate {
func didInput(age: Int) {
self.age = age
let viewController = self.viewController(withType: DisplayerViewController.self)
viewController.age = age
viewController.name = name
navigationController.pushViewController(viewController, animated: true)
}
}
Not really. You can pass view by view the item but it's not a proper way of doing things.
I suggest you to have a Static Manager or this kind of stuff to store the information globally in your app to retrieve it later
All the solution are pretty good. Possible you can try the below model also
1. DataModel class
1.1 Should be singleton class
1.2 Declare value
Step 1 : ViewCOntroller-one
1 Create the Sharedinstance of singleton class
1.1 Assign the value
Step 3 :ViewController-two
1 Create the Sharedinstance of singleton class
1.1 Get the value

How to update UITabBarController from a Helper class?

I have a help class like this:
class NotificationHelper {
var managedObjectContext: NSManagedObjectContext!
init(context: NSManagedObjectContext) {
//super.init()
managedObjectContext = context
}
//functions related to Core Data
//update Badge
func updateBadge() {
var count = 1
let currentCount = self.tabBarController?.tabBar.items?[3].badgeValue
if currentCount != nil {
count = Int(currentCount!)! + 1
}
self.tabBarController?.tabBar.items?[3].badgeValue = String(count)
}
}
I'm just not sure how to get a reference to tabBarController so I can update it. I tried making the class inherit from UIViewController, but I think I was going down the wrong path there.
Also, am I correct in passing managedObjectContext like this? So that this class will be able to access Core Data.
Solved.
Instead of trying to inherit from somewhere, I decided to pass the UITabBarController as a parameter when needed:
func updateTabBarBadge(tabBarController: UITabBarController) {
It just means I have to call updateTabBarBadge every time I want to update it, instead of having other functions update it for me.

Resources