I have a class (Swift 3) in my app that contains the basic CoreData stack which I moved out of AppDelegate. All of the operations the app needs to perform are in another class I call CoreDataHelper. When the app is launched, AppDelegate creates an instance of CoreDataHelper (let dataManager = CoreDataHelper()) which is the only thing that talks to the stack. Then in launchCalculatorViewController(), dataManager asks the stack for a new LiftEvent:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let dataManager = CoreDataHelper()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// check if data needs to be loaded and load it if needed...
launchCalculatorViewController()
return true
}
func launchCalculatorViewController() {
self.window = UIWindow(frame: UIScreen.main.bounds)
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
if let initialViewController: CalculatorViewController = mainStoryboard.instantiateInitialViewController() as? CalculatorViewController {
// create the liftEvent, the viewModel, and wire it up to the view controller
let liftEvent = dataManager.createNewLiftEvent() // go get me a new LiftEvent
let viewModel = calculatorLiftEventViewModelFromLiftEvent(withLiftEvent: liftEvent, dataManager: dataManager)
initialViewController.viewModel = viewModel
initialViewController.dataManager = dataManager
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
}
}
here's the createNewLiftEvent() method in dataManager:
func createNewLiftEvent() -> LiftEvent {
let newLiftEvent = LiftEvent(dataManager: self, insertIntoManagedObjectContext: stack.managedObjectContext)
return newLiftEvent
}
and the initialization of LiftEvent looks like this:
class LiftEvent: NSManagedObject, LiftEventProtocol {
var dataManager: CoreDataHelper!
override func awakeFromInsert() {
super.awakeFromInsert()
let date = Date()
self.date = date
let defaultUnit = UserDefaults.weightUnit()
if defaultUnit == "kg" {
weight = Measurement(value: 0.0, unit: .kilograms)
liftWeights[defaultUnit] = weight
} else {
weight = Measurement(value: 0.0, unit: .pounds)
liftWeights[defaultUnit] = weight
}
if let defaultFormula = dataManager.fetchDefaultFormula() { // unexpected nil error thrown here
self.formula = defaultFormula // this is an NSManagedObject
}
}
convenience init(dataManager: CoreDataHelper, insertIntoManagedObjectContext context: NSManagedObjectContext!) {
let entity = NSEntityDescription.entity(forEntityName: "LiftEvent", in: context)!
self.init(entity: entity, insertInto: context) // exits after this line
self.dataManager = dataManager // never gets executed so of course it's nil
}
}
I get the "unexpectedly found nil while unwrapping an Option value" error because dataManager is nil at this point. I try to set it in the convenience initializer but self.dataManager = dataManager is never executed because it exits right after self.init. Of course I can't put it before self.init because the object has to exist before I can set the value.
I'm passing in dataManager in an attempt to ensure that the Formula (also an NSManagedObject) and this new LiftEvent being created are in the same managed object context so I can set the relationship between them in awakeFromInsert(). Before trying this approach it was crashing because the LiftEvent and Formula were it two different managedObjectContexts (even though I did a lot of looking to make sure that dataManager and the stack were only instantiated once). The approach above was inspired by this thread, but I think I might not understand initialization as well as I thought I did.
How do I pass in the managed object context to create a LiftEvent so it's in the same managed object context as Formula so the relationship can be set? Or, is the totally the wrong approach for this?
Related
In my app, it has two ViewControllers on below same screen.
ViewControllerA is an UIViewController, which includes a tableView.
ViewControllerB is a containerView, which is on top of UITabBarController(root VC) as a child.
For now, I load data(songs array, index, etc.) in viewWillAppear of ViewControllerA, that would be in background queue. Meanwhile, ViewControllerB requires the same data when ViewControllerA's data loading is complete.
Currently, I'm using Dependency Injection to keep a unique data source. Declare modelController reference in ViewController A and B, and use getter and setter to binding the data changes.
The problem is the songs array fetched from modelController in ViewControllerB is nil. Because ViewControllerA will set data on modelController when loading is completed. And ViewControllerB will use it from modelController in its viewWillAppear. But these two VC are on the same screen, how could I guaranty which one will go first? Also data loading is in background queue, so it is suppose to be delayed. Then if ViewControllerB needs that data in first place, it can't be.
My current thought is to load data on ViewControllerB too, so it will not rely on ViewControllerA's result. Or add some observer/listener to message ViewControllerB when data is ready. I think it is kind of architecture stuff, I need advices from experienced guys, any hints are appreciated!
Create and inject the ModelController
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
guard let rootViewController = window?.rootViewController as? TabBarController else {
fatalError("Unexpected Root View Controller")
}
// create dependency injection here
rootViewController.modelController = ModelController()
AudioManager.shared.modelController = rootViewController.modelController
}
ModelController
enum PlayMode: String {
case cycle = "repeat"
case cycleOne = "repeat.1"
case shuffle = "shuffle"
}
class ModelController {
var songs: [Song]? = nil
var position: Int = 0
var playMode = PlayMode.cycle
}
Getter and Setter
var modelController: ModelController!
var songs: [Song]?
{
get { modelController.songs }
set { modelController.songs = newValue }
}
var position: Int
{
get { modelController.position }
set { modelController.position = newValue }
}
Load data in ViewControllerA.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
loadData()
}
func loadData() {
self.showSpinner()
songs?.removeAll()
DispatchQueue.global(qos: .background).async { [weak self] in
self?.songs = DataFetcher.shared.fetchMetaData().0
DispatchQueue.main.async {
print("data load complete")
self?.table.reloadData()
self?.removeSpinner()
}
}
}
so what you said is also possible
observer/listener to message ViewControllerB when data is ready
You can have a shared data class which both the vcA and vcB use. For example
class SharedData {
static let shared = SharedData()
// Use didLoad as a flag for both ViewControllers
var didLoad: Bool {
return self.data != nil
}
var data: SomeData!
private init() {
fetchData()
}
}
In this answer there is a function called fetchData which is in the same class. You can invoke the fetchData function in the background, and use the didLoad or data as flags for displaying/calling any other function you require. This class is also a singleton meaning there will be one instance of this class throughout your applications - So you can use this anywhere in your application.
This is merely an example. You can extend this to your needs such as adding a delegate to listen for callbacks when data is loaded so you can do something in both VCs.
Or you can simply use a protocol from vcA (assuming vcA triggers the network call) and once the data is loaded you can pass it to vcB
I'm working in a subclass of NSViewController where in viewDidLoad I fetch my entities from CoreData as below:
let delegate = AppDelegate.init()
let context = delegate.persistentContainer.viewContext
let fetch = PicPathEntity.entityFetchRequest()
var tempEntities: [PicPathEntity] = []
context.performAndWait {
tempEntities = try! fetch.execute()
}
entities = tempEntities
Then during the itemForRepresentedObjectAt function, it appears that the count of entities is the same, but the property values have become nil/empty.
I did some searching and found similar problems with no clear answers (at least not for Swift)
I can't seem to find information on how to properly fetch and retain the data in my entities variable.
I figured out how to solve this after reading the how the relationship between objects and context work.
Turns out all I needed to make this work, was make the context into a property of the ViewController instead of a local variable of viewDidLoad.
var entities: [PicPathEntity] = []
var context: NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
let delegate = AppDelegate.init()
context = delegate.persistentContainer.viewContext
let fetch = PicPathEntity.entityFetchRequest()
context.performAndWait {
entities = try! fetch.execute()
}
}
Due to helpful comments to this answer, I have now updated the code to:
var entities: [PicPathEntity] = []
var context: NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
let delegate = NSApplication.shared.delegate as! AppDelegate
context = delegate.persistentContainer.viewContext
let fetch = PicPathEntity.entityFetchRequest()
context.perform {
self.entities = try! fetch.execute()
self.picFinderEntityCollectionView.reloadData()
}
configureCollectionView()
}
I have a custom tab bar controller. each tab contains a viewcontroller embedded in a navigation controller. I get a value for pro_user from the database in appledelegate and set it there. then before CustomTabBarController gets launched (in appledelegate), i set it's "pro_user" property to true or false (this works and CustomTabBarController receives the value from appledelegate).
Now i'm trying to pass this same value to the ViewControllers (ViewController1 and ViewController2). each view controller also has a "pro_user" property.I'm doing this by creating the viewcontroller instances and then setting their pro_user property before embedding each viewcontroller in a navigationcontroller. but neither viewcontroller is actually receiving the value of pro_user which i'm setting in CustomTabBarController. I hope this is clear. How do i pass the value of pro_user from CustomTabBarController to each of the view controllers? programmatically (i'm not using storyboards)
class AppDelegate: UIResponder, UIApplicationDelegate {
var pro_user = true
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame:UIScreen.main.bounds)
window?.makeKeyAndVisible()
let customTabBarController = CustomTabBarController()
customTabBarontroller.pro_user = pro_user
self.window?.rootViewController = customTabBarController
return true
}
}
class CustomTabBarController:UITabBarController{
var pro_user : Bool?
override func viewDidLoad(){
super.viewDidLoad()
let viewController1 = ViewController1()
viewController1.pro_user = pro_user //doesn't work
let firstNavigationController = UINavigationController(rootViewController: viewController1)
let viewController2 = ViewController2()
viewController2.pro_user = pro_user //doesn't work
let secondNavigationController = UINavigationController(rootViewController:viewController2)
viewControllers=[firstNavigationController,secondNavigationController]
}
It looks like you're setting a global setting. If so you may want to consider using UserDefaults.
E.g. with a nice extension:
extension UserDefaults {
var isProUser: Bool {
get {
return bool(forKey: "isProUser")
}
set {
set(newValue, forKey: "isProUser")
}
}
}
Then anywhere in your app you can set it:
UserDefaults.standard.isProUser = true
And get it:
let isProUser = UserDefaults.standard.isProUser
The value is also saved between launches.
In my iOS app, I have two Firebase-related functions that I want to call within viewDidLoad(). The first picks a random child with .queryOrderedByKey() and outputs the child's key as a string. The second uses that key and observeEventType to retrieve child values and store it in a dict. When I trigger these functions with a button in my UI, they work as expected.
However, when I put both functions inside viewDidLoad(), I get this error:
Terminating app due to uncaught exception 'InvalidPathValidation', reason: '(child:) Must be a non-empty string and not contain '.' '#' '$' '[' or ']''
The offending line of code is in my AppDelegate.swift, highlighted in red:
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate
When I comment out the second function and leave the first inside viewDidLoad, the app loads fine, and subsequent calls of both functions (triggered by the button action) work as expected.
I added a line at the end of the first function to print out the URL string, and it doesn't have any offending characters: https://mydomain.firebaseio.com/myStuff/-KO_iaQNa-bIZpqe5xlg
I also added a line between the functions in viewDidLoad to hard-code the string, and I ran into the same InvalidPathException issue.
Here is my viewDidLoad() func:
override func viewDidLoad() {
super.viewDidLoad()
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
pickRandomChild()
getChildValues()
}
Here is the first function:
func pickRandomChild () -> String {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
})
return movieToGuess
}
Here is the second function:
func getChildValues() -> [String : AnyObject] {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
})
return movieDict
)
And for good measure, here's the relevant portion of my AppDelegate.swift:
import UIKit
import Firebase
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
FIRApp.configure()
return true
}
I'm guessing Swift is executing the code not in the order I expect. Does Swift not automatically wait for the first function to finish before running the second? If that's the case, why does this pairing work elsewhere in the app but not in viewDidLoad?
Edit: The issue is that closures are not called in order.
I'm not sure what your pickRandomChild() and getChildValues() methods are, so please post them as well, but the way I fixed this type issue was by sending the data through a closure that can be called in your ViewController.
For example when I wanted to grab data for a Full Name and Industry I used this. This method takes a Firebase User, and contains a closure that will be called upon completion. This was defined in a class specifically for pulling data.
func grabDataDict(fromUser user: FIRUser, completion: (data: [String: String]) -> ()) {
var myData = [String: String]()
let uid = user.uid
let ref = Constants.References.users.child(uid)
ref.observeEventType(.Value) { (snapshot, error) in
if error != nil {
ErrorHandling.defaultErrorHandler(NSError.init(coder: NSCoder())!)
return
}
let fullName = snapshot.value!["fullName"] as! String
let industry = snapshot.value!["industry"] as! String
myData["fullName"] = fullName
myData["industry"] = industry
completion(data: myData)
}
}
Then I defined an empty array of strings in the Viewcontroller and called the method, setting the variable to my data inside the closure.
messages.grabRecentSenderIds(fromUser: currentUser!) { (userIds) in
self.userIds = userIds
print(self.userIds)
}
If you post your methods, however I can help you with those specifically.
Edit: Fixed Methods
1.
func pickRandomChild (completion: (movieToGuess: String) -> ()) {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
// Put whatever you want to return here.
completion(movieToGuess)
})
}
2.
func getChildValues(completion: (movieDict: [String: AnyObject]) -> ()) {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
// Put whatever you want to return here.
completion(movieDict)
})
}
Define these methods in some model class, and when you call them in your viewcontroller, you should be able to set your View Controller variables to movieDict and movieToGuess inside each closure. I made these in playground, so let me know if you get any errors.
Your functions pickRandomChild() and getChildValues() are asynchronous, therefore they only get executed at a later stage, so if getChildValues() needs the result of pickRandomChild(), it should be called in pickRandomChild()'s completion handler / delegate callback instead, because when one of those are called it is guaranteed that the function has finished.
It works when you comment out the second function and only trigger it with a button press because there has been enough time between the app loading and you pushing the button for the asynchronous pickRandomChild() to perform it action entirely, allowing getChildValues() to use its returned value for its request.
I had some repetitive UIViewController boiler-plate scattered around that I wanted to encapsulate, so I defined this generic UIViewController extension method:
extension UIViewController {
func instantiateChildViewController<T: UIViewController>(
storyboardName: String? = nil,
identifier: String? = nil
) -> T {
let storyboard: UIStoryboard!
if let name = storyboardName {
storyboard = UIStoryboard(name: name, bundle: nil)
}
else {
storyboard = UIStoryboard(name: "\(T.self)", bundle: nil)
}
let vc: T!
if let identifier = identifier {
vc = storyboard.instantiateViewController(withIdentifier: identifier) as! T
}
else {
vc = storyboard.instantiateInitialViewController()! as! T
}
self.addChildViewController(vc)
self.view.addSubview(vc.view)
return vc
}
}
However, when I use this extension like so:
class ChildViewController: UIViewController { /*...*/ }
class ParentViewController: UIViewController {
private var childVC: ChildViewController!
//...
func setupSomeStuff() {
self.childVC = self.instantiateChildViewController() //<-- Compiler error
let vc: ChildViewController = self.instantiateChildViewController() //<-- Compiles!
self.childVC = vc
}
}
I get the compiler error Cannot assign value of UIViewController to type ChildViewController! on the line with the comment above. However, if I use an intermediate variable that I explicitly give a type to it works.
Is this a Swift bug? (Xcode 8.1) My interpretation of how generics work is that in this case T should equal the more specific ChildViewController, not the less constrained UIViewController. I get the same issue if I defined childVC as private var childVC: ChildViewController?, the only work-around I've found is the local variable, which obviously makes the extension less compelling, or to do an explicit cast like:
self.childVC = self.instantiateChildViewController() as ChildViewController
I've seen this too. I think there's some weird behavior around Optionals the compiler isn't dealing with as expected.
If you change the return value of the function to an optional value it should work without a problem.
func instantiateChildViewController<T: UIViewController>(//whateverParams) -> T!
or
func instantiateChildViewController<T: UIViewController>(//whateverParams) -> T?
Also, your childVC should be a var rather than a let if you're going to set it anyplace other than an initializer