I am writing several Swift multiplayer games based on the Ray Wenderlich tutorial for Nine Knights. (https://www.raywenderlich.com/7544-game-center-for-ios-building-a-turn-based-game)
I use pretty much the same GameCenterHelper file except that I change to a segue instead of present scene since I am using UIKit instead of Sprite Kit with the following important pieces:
present match maker:
func presentMatchmaker() {
guard GKLocalPlayer.local.isAuthenticated else {return}
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 2
request.inviteMessage = "Would you like to play?"
let vc = GKTurnBasedMatchmakerViewController(matchRequest: request)
vc.turnBasedMatchmakerDelegate = self
currentMatchmakerVC = vc
print(vc)
viewController?.present(vc, animated: true)
}
the player listener function:
extension GameCenterHelper: GKLocalPlayerListener {
func player(_ player: GKPlayer, receivedTurnEventFor match: GKTurnBasedMatch, didBecomeActive: Bool) {
if let vc = currentMatchmakerVC {
currentMatchmakerVC = nil
vc.dismiss(animated: true)
}
guard didBecomeActive else {return}
NotificationCenter.default.post(name: .presentGame, object: match)
}
}
The following extension for Notification Center:
extension Notification.Name {
static let presentGame = Notification.Name(rawValue: "presentGame")
static let authenticationChanged = Notification.Name(rawValue: "authenticationChanged")
}
In the viewdidload of the menu I call the following:
override func viewDidLoad() {
super.viewDidLoad()
createTitleLabel()
createGameImage()
createButtons()
GameCenterHelper.helper.viewController = self
GameCenterHelper.helper.currentMatch = nil
NotificationCenter.default.addObserver(self, selector: #selector(authenticationChanged(_:)), name: .authenticationChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(presentGame(_:)), name: .presentGame, object: nil)
}
and tapping the multi device buttons calls the following:
#objc func startMultiDeviceGame() {
multiPlayer = true
GameCenterHelper.helper.presentMatchmaker()
}
and the notifications call the following:
#objc func presentGame(_ notification: Notification) {
// 1
print("present game")
guard let match = notification.object as? GKTurnBasedMatch else {return}
loadAndDisplay(match: match)
}
// MARK: - Helpers
private func loadAndDisplay(match: GKTurnBasedMatch) {
match.loadMatchData { [self] data, error in
if let data = data {
do {
gameModel = try JSONDecoder().decode(GameModel.self, from: data)
} catch {gameModel = GameModel()}
} else {gameModel = GameModel()}
GameCenterHelper.helper.currentMatch = match
print("load and display")
performSegue(withIdentifier: "gameSegue", sender: nil)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
print("prepare to segue")
if let vc = segue.destination as? GameVC {vc.gameModel = gameModel}
}
Which is hopefully easy to follow.
The game starts and the menu scene adds the observer for present game
The player taps multi device, which presents the matchmaker
The player selects their game from the match maker, which I think activates the player listener function
This posts to the Notification Center for present game
The notification center observer calls present game, which calls load and display, with a little help from prepare segue
My issue is that the first time I do this it works perfectly, and per the framework from that tutorial that I can't figure out how to change (an issue for a different question I think) after a player takes their turn they are returned to the menu. The second time they enter present matchmaker and select a game the present game function is called twice, and the third time they take their turn without shutting down the app it is called 3 times, etc. (I have the print statements in both the present game and load and display functions and they are called back to back the 2nd time through and back to back to back the 3rd time etc. even though they are only called once the first time a game is selected from the matchmaker)
Console messages
present matchmaker true
<GKTurnBasedMatchmakerViewController: 0x104810000>
present game
present game
present game
load and display
prepare to segue
load and display
prepare to segue
load and display
prepare to segue
2021-03-20 22:32:26.838680-0600 STAX[4997:435032] [Presentation] Attempt to present <STAX.GameVC: 0x103894c00> on <Game.MenuVC: 0x103814800> (from < Game.MenuVC: 0x103814800>) whose view is not in the window hierarchy.
(419.60100000000006, 39.0)
2021-03-20 22:32:26.877943-0600 STAX[4997:435032] [Presentation] Attempt to present <STAX.GameVC: 0x103898e00> on < Game.MenuVC: 0x10501c800> (from < Game.MenuVC: 0x10501c800>) whose view is not in the window hierarchy.
I had thought that this was due to me not removing the Notification Center observers, but I tried the following in the view did load for the menu screen (right before I added the .presentGame observer):
NotificationCenter.default.removeObserver(self, name: .presentGame, object: nil)
and that didn't fix the issue, so I tried the following (in place of the above):
NotificationCenter.default.removeObserver(self)
and that didn't work so I tried them each, one at a time in the view did disappear of the game view controller (which I didn't think would work since self refers to the menu vc, but I was getting desperate) and that didn't work either.
I started thinking that maybe I'm not adding multiple observers that are calling present game more than once, since the following didn't work at all the second time (I'm just using a global variable to keep track of the first run through that adds the observers and then not adding them the second time):
if addObservers {
NotificationCenter.default.addObserver(self, selector: #selector(authenticationChanged(_:)), name: .authenticationChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(presentGame(_:)), name: .presentGame, object: nil)
addObservers = false
}
since it is trying to add a view that is not in the view hierarchy. (Although the background music for that screen starts playing, the menu remains and the game board is not shown...)
I wasn't sure if I'm removing the Notification Center observers incorrectly or if they aren't really the source of the problem so I decided to ask for help :)
Thank you!
I figured it out. I was trying to remove the Notifications from a deallocated instance of the view controller per the below link (The bottom most answer):
How to avoid adding multiple NSNotification observer?
The correct way to remove the notifications is in the view will disappear function like this:
override func viewWillDisappear(_ animated: Bool) {
NotificationCenter.default.removeObserver(self, name: Notification.Name.presentGame, object: nil)
NotificationCenter.default.removeObserver(self, name: Notification.Name.authenticationChanged, object: nil)
}
After implementing that I stopped making multiple calls to the notification center.
Related
In my app, there is a ViewController.swift file and a popupViewController.swift file. Inside the app, when I open the popupViewController with storyboard segue as presentModally and then come back from popupViewController to ViewController with the code dismiss(), the methods viewDidLoad, viewWillAppear, viewDidAppear, ViewWillLayoutSubviews etc. nothing works, they execute just once and don't repeat when I go and return back. So, I want to execute the code every time when viewController.swift is active. I couldn't find a useful info in stackoverflow about this.
Meanwhile, I don't know much about notification and observers(if certainly needed), therefore, can you tell step by step in detail how to do that in Swift (not objective-c)? I mean how to determine if current view controller is active.
Edit: I am navigating from StoryBoard segue, presentModally. There is no Navigation Controller in storyboard.
I tried some codes but nothing happens. The point I came so far is:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector:#selector(appWillEnterForeground), name:UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func appWillEnterForeground() {
print("asdad") //nothing happens
if self.viewIfLoaded?.window != nil {
// viewController is visible
print("CURRENT VİEW CONTROLLER") //nothing happens
}
}
As mention in my comments, I don't use storyboards. There may be a way to create an unwind segue - or maybe not - but [here's a link][1] that may help you with a storyboard-only way of fixing your issue. A quick search on "modal" turned up 9 hits, and the second one starts going into details.
I'm thinking the issue is with what modality is. Basically, your first view controller, which properly executed viewDidAppear, is still visible. So it's effectively not executing viewDidDisappear when your second VC is presented.
You might want to change your concept a bit - an application window (think AppDelegate and/or SceneDelegate become active, where a UIViewController has a is initialized and deinitialized, along with a root UIView that is loaded, appears* and disappears*. This is important, because what you want to do is send your notification from the modal VC's viewDidDisappear override.
First, I find it easiest to put all your notication definitions in an extension:
extension Notification.Name {
static let modalHasDisappeared = Notification.Name("ModalHasDisappeared")
}
This helps not only reduce string typos but also is allows Xcode's code completion to kick in.
Next, in your first view controller, ad an observer to this notification:
init() {
super.init(nibName: nil, bundle: nil)
NotificationCenter.default.addObserver(self, selector: #selector(modalHasDisappeared), name: .modalHasDisappeared, object: nil)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
NotificationCenter.default.addObserver(self, selector: #selector(modalHasDisappeared), name: .modalHasDisappeared, object: nil)
}
#objc func modalHasDisappeared() {
print("modal has disappeared")
}
I've added both forms of init for clarity. Since you are using a storyboard, I'd expect that init(coder:) is the one you need.
Finally, just send the notification when the modal has disappeared:
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.post(name: .modalHasDisappeared, object: nil, userInfo: nil)
}
This sends no data, just the fact that the modal has disappeared. If you want to send data - say, a string or a table cell value, change the object parameter to it:
NotificationCenter.default.post(name: .modalHasDisappeared, object: myLabel, userInfo: nil)
And make the following changes in your first VC:
NotificationCenter.default.addObserver(self, selector: #selector(modalHasDisappeared(_:)), name: .modalHasDisappeared, object: nil)
#objc func modalHasDisappeared(_ notification:Notification) {
let label = notification.object as! UILabel!
print(label.text)
}
Last notes:
To repeat, note that by declaring an extension to Notification.Name, I've only have one place where I'm declaring a string.
There is no code in AppDelegate or SceneDelegate, nor any references to `UIApplication(). Try to think of the view (and view controller) as appearing/disappearing, not background/foreground.
While the first view is visually in the background, it's still visible. So the trick is to code against the modal view disappearing instead.
I am having a little bit of trouble. I am trying to use the notification center to alert the app that some content is done loading. After the user signs up or logs in successfully I create the view controller and then and then make it the root view controller using the blow
func finishLoggingIn() {
// print("Finish logging in from LoginController")
let homeController = HomeViewController()
self.loginButton.stopAnimation(animationStyle: .expand, completion: {
self.view.window?.rootViewController = homeController
self.view.window?.makeKeyAndVisible()
})
}
The login button just creates a loading animation on the button for UI purposes.
When I first enter the controller I add the observer for the notification.
let MainVCSetup = Notification.Name("mainVCComplete")
NotificationCenter.default.addObserver(self, selector: #selector(handleRootViewSwitch), name: MainVCSetup, object: nil)
When the content in my mainVC is done loading I post this same notification to the Notification Center like so
NotificationCenter.default.post(name: MainVCSetup, object: nil)
However this function never fires off no matter what I do
#objc func handleRootViewSwitch(){
print("Trying to handle root view switch attack")
NotificationCenter.default.removeObserver(self, name: MainVCSetup, object: nil)
}
If anyone notices where I went wrong I would greatly appreciate it.
I am currently developing a very simple Live Scores MAC OSX app for personal use where I show a bunch of labels (scores) on the touch bar. What I am trying to achieve in a few steps:
Fetch live soccer scores from a 3rd party API every 30 seconds
Parse the scores and make them into labels
Update the touch bar with new scores
[Please note here that this app will not be published anywhere, and is only for personal use. I am aware of the fact that Apple strictly advises against such type of content in the Touch Bar.]
Here is the code that I wrote following basic Touch Bar tutorial from RW (https://www.raywenderlich.com/883-how-to-use-nstouchbar-on-macos). Skeleton of my code is picked from the RW tutorial:
In WindowController (StoryBoard entry point), override makeTouchBar like this:
override func makeTouchBar() -> NSTouchBar? {
guard let viewController = contentViewController as? ViewController else {
return nil
}
return viewController.makeTouchBar()
}
In ViewController, which is also the Touch Bar Delegate, implement the makeTouchBar fn:
override func makeTouchBar() -> NSTouchBar? {
let touchBar = NSTouchBar()
touchBar.delegate = self
touchBar.customizationIdentifier = .scoresBar
touchBar.defaultItemIdentifiers = [.match1, .flexibleSpace, .match2, ... , .match10]
return touchBar
}
NSTouchBarDelegate in ViewController. scores is where I store my fetched scores (See 5). I return nil for views if scores aren't fetched yet:
extension ViewController: NSTouchBarDelegate {
func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
if (<scores not fetched yet>) {
return nil
}
// matchNum is the match number for which I am showing scores for
let customViewItem = NSCustomTouchBarItem(identifier: identifier)
customViewItem.view = NSTextField(labelWithString: self.scores[matchNum ?? 0])
return customViewItem
}
}
To fetch scores periodically I am running a scheduled task Timer in viewDidLoad() of my viewcontroller like this:
_ = Timer.scheduledTimer(timeInterval: 30.0, target: self, selector: #selector(ViewController.fetchScores), userInfo: nil, repeats: true)
And finally, this is my fetchScores function that also makes a call to update the Touch Bar:
#objc func fetchScores() {
let url = "<scores api end point>"
Alamofire.request(url).responseJSON { response in
if let json = response.result.value {
// update self.scores here and fill it with latest scores
if #available(OSX 10.12.2, *) {
//self.touchBar = nil
self.touchBar = self.makeTouchBar() // This is where I am calling makeTouchBar again to update Touch Bar content dynamically
}
}
}
My understanding from the code above is that once I make a call to makeTouchBar in fetchScores and assign it to my viewcontroller's touchBar property, it should ideally call touchBar(:makeItemForIdentifier) delegate function to update the Touch Bar view (SO thread on this). But in my case, touchBar(:makeItemForIdentifier) is never called. The only time touchBar(:makeItemForIdentifier) is called is the first time, when makeTouchBar is called from my WindowController (See point 1 above). And since scores have not been retrieved yet, my touch bar remains empty.
I have a music app and I wish to determine if playback has been paused while the app was closed (due to an event like a phone call or AirPods being taken out of ear etc)
My first approach was to run a func inside of viewWillAppear that checked
if mediaPlayer.playbackState == .paused {
...
}
If it was paused I updated the play/pause button image. However, this did not work, the play/pause button would still show Play even if it was paused.
Next, I tried adding an observer to the viewDidLoad
NotificationCenter.default.addObserver(self, selector: #selector(self.wasSongInterupted(_:)), name: UIApplication.didBecomeActiveNotification, object: self.mediaPlayer)
The self.wasSongInterupted I call is
#objc func wasSongInterupted(_ notification: Notification) {
DispatchQueue.main.async {
if self.mediaPlayer.playbackState == .paused {
print("paused")
self.isPlaying = false
self.playPauseSongButton.isSelected = self.isPlaying
} else if self.mediaPlayer.playbackState == .playing {
self.isPlaying = true
self.playPauseSongButton.isSelected = self.isPlaying
}
}
}
However, I am still having the same issue.
What is the best way to determine if my music player is playing or paused when I reopen the app?
Edit 1: I Edited my code based on comments.
wasSongInterrupted was not being called, and through breakpoints and errors I discovered the code was mostly not needed. I changed my code to be
func wasSongInterrupted() {
DispatchQueue.main.async {
if self.mediaPlayer.playbackState == .interrupted {
var isPlaying: Bool { return self.mediaPlayer.playbackState == .playing }
print("Playback state is \(self.mediaPlayer.playbackState.rawValue), self.isPlaying Bool is \(self.isPlaying)")
self.playPauseSongButton.setImage(UIImage(named: "playIconLight"), for: .normal)
//self.playPauseSongButton.isSelected = self.isPlaying
}
}
}
and inside my AppDelegate's applicationDidBecomeActive I have
let mediaPlayerVC = MediaPlayerViewController()
mediaPlayerVC.wasSongInterupted()
Now the code runs, however I have an issue.
If I run the following code:
if self.mediaPlayer.playbackState == .interrupted {
print("interrupted \(self.isPlaying)")
}
and then make a call and come back to the app it will hit the breakpoint. It will print out interrupted as well as false which is the Bool value for self.isPlaying
However if I try to update the UI by
self.playPauseSongButton.isSelected = self.isPlaying
or by
self.playPauseSongButton.setImage(UIImage(named: "playIconLight.png"), for: .normal)
I get an error message Thread 1: EXC_BREAKPOINT (code=1, subcode=0x104af9258)
You trying to update you player UI from viewWillAppear. From Apple Documentation:
viewWillAppear(_:)
This method is called before the view controller's view is about to be added to a view hierarchy and before any animations are configured for showing the view.
So if your app was suspended and the becomes active again, this method won't be called, because your UIViewController is already at Navigations Stack.
If you want to catch the moment when your app becomes active from suspended state, you need to use AppDelegate. From Apple Documentation:
applicationDidBecomeActive(_:)
This method is called to let your app know that it moved from the inactive to active state. This can occur because your app was launched by the user or the system.
So you need to use this method at your AppDelegate to handle app running and update your interface.
UPDATE
You saying the inside this AppDelegate method you're doing
let mediaPlayerVC = MediaPlayerViewController()
mediaPlayerVC.wasSongInterupted()
That's wrong because you're creating a new view controller. What you need to do, is to access you existing view controller from navigation stack and update it.
One of the possible solutions is to use NotificationCenter to send a notification. You view controller should be subscribed to this event of course.
At first, you need to create a notification name
extension Notification.Name {
static let appBecameActive = Notification.Name(rawValue: "appBecameActive")
}
Then in you AppDelegate add following code to post your notifications when app becomes active
func applicationDidBecomeActive(_ application: UIApplication) {
NotificationCenter.default.post(name: .appBecameActive, object: nil)
}
And finally in your view controller add to subscribe it on notifications
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(wakeUp),
name: .appBecameActive,
object: nil)
...
}
#objc func wakeUp() {
// Update your UI from here
}
Hope it helps you.
I’m creating a weightlifting calculator application (Swift 4) using MVVM and have been trying for 2 days to figure out why a view model that should have died is still responding to a UserDefaults.defaultsDidChange event notification.
I launch the app:
At launch, in the AppDelegate, I create a new lift event object and use it to initialize a new CalculatorLiftEventViewModelFromLiftEvent for the `CalculatorViewController':
I calculate a lift and save it
I tap the + button to create a new lift:
this causes a new, empty lift event object to be created
this new lift event object is used to initialize a new CalculatorLiftEventViewModelFromLiftEvent object
this new CalculatorLiftEventViewModelFromLiftEvent is then assigned to the CalculatorViewController's viewModel property, replacing the one created when the app launched
the values on the calculator screen are zeroed out, ready for a new lift event to be entered
I tap the Settings button to go to Settings where I change the Formula associated with the current lift event.
The new Formula is saved as the default and the UserDefaults.defaultsDidChange notification is fired
HERE’S THE PART I CAN’T FIGURE OUT: the original view model is still alive and it’s still listening for UserDefault notifications. When I close the Settings screen and go back to the Calculator view, the values from the prior lift event that had been cleared out now reappear.
Here’s what happens when the + (new) button on the Calculator screen is tapped:
#objc fileprivate func onNewButtonTapped(_ sender: UIBarButtonItem) {
let newLiftEvent = dataManager.createNewLiftEvent()
viewModel = CalculatorLiftEventViewModelFromLiftEvent(withLiftEvent: newLiftEvent, dataManager: dataManager)
setupView()
}
Here’s how the CalculatorLiftEventViewModelFromLiftEvent is initialized:
init(withLiftEvent liftEvent: LiftEventRepresentable, dataManager: CoreDataHelper) {
self.modelLiftEvent = liftEvent
self.liftName = Dynamic("\(modelLiftEvent.lift.liftName)")
self.weightLiftedTextField = Dynamic(modelLiftEvent.liftWeight.value)
self.repetitionsTextField = Dynamic("\(modelLiftEvent.repetitions)")
self.oneRepMaxTextField = Dynamic(modelLiftEvent.oneRepMax.value)
self.unitsTextField = Dynamic("\(UserDefaults.weightUnit())")
self.weightPercentages = Dynamic( [ : ] )
self.dataManager = dataManager
super.init()
subscribeToNotifications()
}
UPDATE: Here are the deinit and the addObservers in CalculatorLiftEventViewModelFromLiftEvent. Notice I'm not using block-based observations.
deinit {
print("I got to the deinit method")
unsubscribeFromNotifications()
}
func subscribeToNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(liftNameDidChangeNotification(_:)),
name: NSNotification.Name(rawValue: LiftEventNotifications.LiftNameDidChangeNotification),
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(weightUnitDidChangeNotification(_:)),
name: NSNotification.Name(rawValue: LiftEventNotifications.WeightUnitDidChangeNotification),
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(roundingOptionDidChangeNotification(_:)),
name: NSNotification.Name(rawValue: UserDefaultsNotifications.roundingOptionDidChangeNotification),
object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.defaultsDidChange), name: UserDefaults.didChangeNotification,
object: nil)
}
--- END UPDATE
I pass the modelLiftEvent when segueing to the SettingsViewController:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let identifier = segue.identifier {
switch identifier {
case a:...
case b:...
case "SettingsSegue":
if let nav = segue.destination as? UINavigationController {
let destinationViewController = nav.topViewController as! SettingsViewController
destinationViewController.dismissalDelegate = self
let settingsViewModel = SettingsViewModelFromLiftEvent(withLiftEvent: self.viewModel.modelLiftEvent)
destinationViewController.settingsViewModel = settingsViewModel
destinationViewController.dataManager = dataManager
settingsViewModel.dataManager = dataManager
}
Finally, in CalculatorLiftEventViewModelFromLiftEvent, I’ve put a break point here because this is called when the view model hears the UserDefaults.defaultsDidChange notification. At this point, I have also verified that this CalculatorLiftEventViewModelFromLiftEvent is the old one, not the new one created when I tapped the + button:
#objc func defaultsDidChange(_ notification: Notification) {
let oneRepMax = modelLiftEvent.calculateOneRepMax()
guard oneRepMax.value != 0.0 else { return }
let weightPercentages = getWeightPercentages(weight: oneRepMax.value)
self.weightPercentages.value = weightPercentages
weightLiftedTextField.value = modelLiftEvent.liftWeight.value
repetitionsTextField.value = "\(modelLiftEvent.repetitions)"
oneRepMaxTextField.value = modelLiftEvent.oneRepMax.value
}
I've read through a bunch of documentation about the life cycle of objects but haven't found anything that helps. I expect that when the new CalculatorLiftEventViewModelFromLiftEvent is created and assigned to the `CalculatorViewController''s viewModel property, it would replace the reference to the old one and it would cease to exist. Evidently, that's not what's happening.
Does anyone have any idea why when I go from the Calculator view (step 3) that has no values (except for 0.0) to the Settings and then come back, the prior lift event values are displayed?
I've fixed the problem of the prior liftEvent being displayed after clearing the calculator, changing the default formula, and coming back to the calculator screen.
On CalculatorViewController, when the + button is tapped, instead of creating a new viewModel and assigning it to the viewModel property, I'm asking my AppDelegate to create both a new CalculatorViewController and CalculatorLiftEventViewModelFromLiftEvent by using the launchCalculatorViewController method which does this when the app launches.
The original code in CalculatorViewController:
#objc fileprivate func onNewButtonTapped(_ sender: UIBarButtonItem) {
let newLiftEvent = dataManager.createNewLiftEvent()
viewModel = CalculatorLiftEventViewModelFromLiftEvent(withLiftEvent: newLiftEvent, dataManager: dataManager)
self.percentagesTableView.reloadData()
setupView()
}
Now the new code in CalculatorViewController:
#objc fileprivate func onNewButtonTapped(_ sender: UIBarButtonItem) {
(UIApplication.shared.delegate as? AppDelegate)?.launchCalculatorViewController()
}
and in AppDelegate:
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 {
self.window?.rootViewController = initialViewController
let liftEvent = dataManager.createNewLiftEvent()
let viewModel = CalculatorLiftEventViewModelFromLiftEvent(withLiftEvent: liftEvent, dataManager: dataManager)
initialViewController.viewModel = viewModel
initialViewController.dataManager = dataManager
self.window?.makeKeyAndVisible()
}
}
Unfortunately, I determined that CalculatorLiftEventViewModelFromLiftEvent objects are never being deallocated which tells me I've got a strong reference cycle that won't let go:
That will have to be another SO question.