Passing data back to controllers when struct model modified - swift

I have a series of View Controllers which pass a struct model object down the chain.
If a user modifies the value of a property on the model, I update the view controller's model instance, and now I need to inform the parent view controllers that this object's value has changed.
Previously I would have used classes over structs for my model object and so I wouldn't have this issue as the object would have been directly written to.
But since structs are pass by value, I have to update the state on other view controllers. I have been using a singleton Manager object to handle state changes through a call to updateModel(). Is there a better way?

I have used something similar to this; keep a reference to the neighbouring view controller (with care to avoid a reference cycle) and a property observer on the struct property to update it when it changes.
This could also be updated prior to presenting a new view controller or before a segue, depending on your needs.
class myViewController: UIViewController {
// Your struct
var model: MyStruct? {
didSet {
if let pvc = previousVC {
pvc.model = model
}
}
}
// Keep a reference to the previous view controller on your stack
var previousVC: UIViewController?
override viewDidLoad() {
super.viewDidLoad()
self.model = MyStruct()
}
}

Related

Notifications when an ObservedObject is changed

I have a graphical user interface made of several SwiftUI components (lets call them subviews).
These subviews communicate with each other with the help of ObservableObject / ObservedObject.
When changes are made to one view, the other view is automatically notified and updates itself.
However, one of the subviews is not a SwiftUI view but a SpriteKit SKScene.
Here, too, I would like to react to the changes in an observed value.
But I do not have a view that updates automatically. I want to make adjustments to the sprites, depending on the observed value.
How can I be notified of changes in the value?
Can I call a method as soon as the value of the ObservedObject changes?
It is easy to observe changes in ObservedObject from UIKit or SKScene. The example below is from UIViewController, change this for the equivalent to SKScene:
import Combine
class Object: ObservableObject { 
#Published var value: Int = 10
#Published var anotherValue: String = "Hello"
}
class MyViewController: UIViewController { 
let observedObject = Object()
var cancellableBag = Set<AnyCancellable>()
override func viewDidLoad() { 
super.viewDidLoad()
// REACT TO ANY #PUBLISHED VARIABLE
observedObject.objectWillChange.sink { 
// Do what you want here
}.store(in: &cancellableBag)
// REACT ONLY TO ONE SPECIFIED #PUBLISHED VARIABLE
observedObject.$value.sink { value in
// Do what you want here
}.store(in: &cancellableBag)
}
}
Note that #Published is a propertyWrapper with projectedValue. The projectedValue in this case is the Publisher, that emits events every time the wrappedValue will be changed, so in order to access to the projectedValue it is necessary to call $value instead of value.

Sharing ViewModel between SwiftUI components

I'd like to implement a navigator/router for an architecture implemented with SwiftUI and Combine. In a few words the View will share viewModel with Router. When the View triggers a change on the viewModel the Router should navigate to a new sheet.
This is a version of my code where I'm directly passing the viewModel from View to Router. Is there anything wrong? My biggest doubt is that since I'm using #ObservedObject on both the Router and the View, two different instances of the viewModel are created.
VIEW MODEL
class BootViewModel:ObservableObject{
#Published var presentSignIn = false
}
VIEW
struct BootView: View {
#ObservedObject var viewModel:BootViewModel
var navigator:BootNavigator<BootView>? = nil
init(viewModel:BootViewModel) {
self.viewModel = viewModel
self.navigator = BootNavigator(view: self, viewModel: viewModel)
self.navigator.setSubscriptions()
}
var body: some View {
VStack{
Text("Hello")
Button("Button"){
self.viewModel.presentSignIn.toggle()
}
}
}
}
NAVIGATOR
class BootNavigator<T:View>{
var view:T? = nil
#ObservedObject var viewModel:BootViewModel
init(view:T, viewModel:BootViewModel) {
self.view = view
self.viewModel = viewModel
}
func setSubscriptions(){
subscribe(onSigninPressed: $viewModel.presentSignIn)
}
func subscribe(onSigninPressed : Binding<Bool>){
_ = view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
}
}
}
Why the SignInView is never presented?
Without taking into account the fact that using a router with swiftUI is not needed in general(I'm mostly doing an exercise)... is there anything wrong with this implementation?
This
view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
MUST be somewhere in body (directly or via computed property or func) but inside body's ViewBuilder
Some notes I have to point out here:
ValueType
There is a difference between an UIView and a SwiftUI View. All SwiftUI Views are value type! So they get copied when you pass them around. Be aware of that.
Single instance
If you want a single instance like a regular navigator for your entire app, you can use singleton pattern. But there is a better approach in SwiftUI universe called #Environment objects. You can take advantage of that.
Trigger a view refresh
To refresh the view (including presenting something), you must code inside the var body. But it can be directly written on indirectly through a function or etc.

View Model composition breaks mvvm structure

In mvvm, the view is never able to access the model.
I define "View model composition" as a concept where a view model may have 1-to-many child view models
If a parent view model needs to mutate the model of 1 of its child view models, then if the view has access to that view model it would inherently have access to mutate the child vm's models.
What approach can I use to enforce "view never accesses model" rule?
Example code in Swift
class BigVm{
let accountVm: AccountViewModel
let anotherVm: AnotherSubviewViewModel
private func someEventHappened(){
//some logic that mutates accountVm's model based on state of anotherVm and vise versa
accountVm.mutateOrAccessModel()
}
}
class BigViewController: UIViewController{
let viewModel: BigVm
let subviewAccount: AccountView //has a viewModel of AccountViewModel
let anotherSubview: AnotherSubview //has a viewModel of AnotherSubviewViewModel
func viewDidLoad(){
super.viewDidLoad()
subviewAccount.vm = viewModel.accountVm
anotherSubview.vm = viewModel.anotherVm
//now what stops BigViewController to do the next lines
viewModel.subViewAccount.mutateOrAccessModel()
}
}
My solution to this is accessing the viewModels as protocols in the view layer. Though that adds a lot of boilerplate just to enforce mvvm.

What is the best way to reload a child tableview contained within a parent VC that is being fed fresh data?

I have been playing with the concept of the parent/child view delegation for a few days now, and currently understand how to feed data from parent to child. However, now, I want a button in the parent (main VC) to reload the data presented in the child VC.
I'm trying to delegate a method that is activated in the child VC's class but is activated in the parent's navigation controller. So that when I press the button, the delegated method in the child VC is performed; in my case, that method would be reload table. Why am I getting so many errors when trying to set up this simple delegation relationship?
My parent/container View is currently delegating a method to the child, so I have it set up from child -> parent. But I want to set it up from parent -> child. Pretty much I have:
struct Constants {
static let embedSegue = "containerToCollectionView"
}
class ContainerViewController: UIViewController, CollectionViewControllerDelegate {
func giveMeData(collectionViewController: CollectionViewController) {
println("This data will be passed")
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == Constants.embedSegue {
let childViewController = segue.destinationViewController as! CollectionViewController
childViewController.delegate = self
}
}
FROM CHILD:
protocol CollectionViewControllerDelegate {
func giveMeData(collectionViewController: CollectionViewController)
}
class CollectionViewController: UIViewController {
var delegate:CollectionViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
self.delegate?.giveMeData(self)
// Do any additional setup after loading the view.
}
I think my trouble is the fact that I'm declaring the child delegate in a prepareforsegue, so that was straight forward, but now I want the reverse delegation. How do I set that up so that I can use a child-method from the parent VC?
The child view controller has no business supplying other controllers with data. It should actually not even have any data fetching logic that is so generic it is also used by other controllers. You should refactor the data methods out into a new class.
This pattern is called Model-View-Controller, or MVC, and is a very basic concept that you should understand and follow. Apple explains it pretty well.
In general, to send data to from a controller to a detail controller, use prepareForSegue to set properties, etc. To communicate back to the parent controller, you use delegate protocols, but usually these are called when the detail controller is finished with its work and just reports the result up to the parent.
If you want to update the detail VC with new data (without dismissing it and with the parent not visible) you should not put the logic to update it into the parent. Instead, use the structure suggested above.

Passing data between tab viewed controllers in swift?

What it does
When the first page in my tab bar controller loads, I retrieve data from a json file
I store it in an array (in the first view controller)
The data obtained will be displayed in the second view controller. The data is already loaded and stored in an array in the first view controller.
Problem:
I can't figure out a way to pass the data between the two view controllers. Can't pass data based on the segue identifier since it is a tab bar controller
Please help!
If you need to pass the data between view controllers then :
var secondTab = self.tabBarController?.viewControllers[1] as SecondViewController
secondTab.array = firstArray
I ended up using a singleton as Woodster suggested in his answer above.
In Swift 3
Create a new swift file and create a class:
class Items {
static let sharedInstance = Items()
var array = [String]()
}
In any of your view controllers you can access your array like this:
Items.sharedInstance.array.append("New String")
print(Items.sharedInstance.array)
H. Serdar's code example is right, that's the way to access another tab's view controller and give it data.
Keep in mind that when you pass an array in Swift, you're passing it by value, unlike Objective-C, which passes it by reference. This means that changes made by your second view controller won't be reflected in your first view controller, because your second one is using a copy of the array, not the same array. If you want both view controllers to modify the same array, put the array in a class, and pass a single instance of that class around.
Some other considerations:
You could subclass the TabBarController to give it a property that'll store your data, and that would be available to all tabs using:
if let tbc = tabBarController as? YourCustomTabBarSubclass {
println("here's my data \(tbc.array)")
}
In that situation, you'd be accessing the same array from multiple tabs, so changes in one tab would be reflected elsewhere.
I recommend against the approach of using your App Delegate as a centralized place to store data. That's not the purpose of the application's delegate. Its purpose is to handle delegate calls for the application object.
View Controllers should have all the data, encapsulated within them, that they need to do their job. They have a connection to their model data (such as your array, or a reference to a database or a managed object context) as opposed having a view controller reach out to another object by traversing a view controller graph or going into the delegate or even using a global variable. This modular, self contained construction of View Controllers lets you restructure your app for similar but unique designs on different devices, such as presenting a view controller in a popover on one device (like an iPad) and presenting it full screen on another, such as an iPhone.
SWIFT 3
In your first viewcontroller, declare your variable (in your case an array) like you normally would.
In your second viewcontroller, do this:
var yourVariable: YourVariableClass {
get {
return (self.tabBarController!.viewControllers![0] as! FirstViewControllerClass).yourVariable
}
set {
(self.tabBarController!.viewControllers![0] as! FirstViewControllerClass).yourVariable = newValue
}
}
This works because, in a tabbarcontroller all viewcontrollers behind the tab items are initialized. By doing this in your second viewcontroller you are actually getting/setting the variable from/in the first viewcontroller.
For Xcode 11 & Swift 5 + Storyboard + Dependency Injection Approach
Assuming you are using a storyboard this is a method I have devised.
Step 1:
Put an identifier on your tabBarController like I did in the image below.
Step 2:
In the scenedelegate.swift file (NOT appDelegate.swift), add the following code to the appropriate func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { method.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
self.window = self.window ?? UIWindow()//#JA- If this scene's self.window is nil then set a new UIWindow object to it.
//#Grab the storyboard and ensure that the tab bar controller is reinstantiated with the details below.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let tabBarController = storyboard.instantiateViewController(withIdentifier: "tabBarController") as! UITabBarController
for child in tabBarController.viewControllers ?? [] {
if let top = child as? StateControllerProtocol {
print("State Controller Passed To:")
print(child.title!)
top.setState(state: stateController)
}
}
self.window!.rootViewController = tabBarController //Set the rootViewController to our modified version with the StateController instances
self.window!.makeKeyAndVisible()
print("Finished scene setting code")
guard let _ = (scene as? UIWindowScene) else { return }
}
You will notice that the scenedelgate.swift file has a member variable; var window: UIWindow?. This used to be part of appDelegate but was changed in xCode 11 and Swift 5 so a lot of similar answers and tutorials will be out of date.
The part in the code that says storyboard.instantiateViewController(withIdentifier: you will want to add the name you used for the parameter. In my screenshot you will see I called it tabBarController.
To make this function work on any type of viewController without having to instantiate each one separately on an index, I've used a protocol strategy called StateControllerProtocol. We will be creating this next along with the StateController which will hold the global variables.
Step 3:
In stateController.swift or whatever you want to name this file, add the following code removing aspects that do not apply to your project.
import Foundation
struct tdfvars{
var lateBED:Double = 0.0
var acuteBED:Double = 0.0
var rbe:Double = 1.4
var t1half:Double = 1.5
var alphaBetaLate:Double = 3.0
var alphaBetaAcute:Double = 10.0
var totalDose:Double = 6000.00
var dosePerFraction:Double = 200.0
var numOfFractions:Double = 30
var totalTime:Double = 168
var ldrDose:Double = 8500.0
}
//#JA - Protocol that view controllers should have that defines that it should have a function to setState
protocol StateControllerProtocol {
func setState(state: StateController)
}
class StateController {
var tdfvariables:tdfvars = tdfvars()
}
The variables you want to share between views I recommend adding to the struct. I named mine tdfvariables but you will want to name this something relevant to your project. Note the protocol defined here as well. This is a protocol that will be added to each viewController as an extension that defines that there should be a function to set its stateController member variable (which we have not defined yet, but will in a later step).
Step 4:
In my case I have 2 views controlled by the tabBarController. StandardRegimenViewController and settingsViewController. This is the code you will want to add for your viewControllers.
import UIKit
class SettingsViewController: UIViewController {
var stateController: StateController?
override func viewDidLoad() {
super.viewDidLoad()
}
}
//#JA - This adds the stateController variable to the viewController
extension SettingsViewController: StateControllerProtocol {
func setState(state: StateController) {
self.stateController = state
}
}
The extension here adds the protocol to your class and adds the function as required by it that we defined earlier in the stateController.swift file. This is what will eventually get the stateController and it's struct values into your viewController.
Step 5:
Use the stateController to get access to your variables! You are done!
Here is some examples of how I did this in one of my controllers.
stateController?.tdfvariables.lateBED = 100
You can read the variables the same way! The advantage of this approach is you are NOT using Singletons and instead Dependency Injection for your viewControllers and anything else that may need access to your variables. Read more about dependency injection to see the benefits vs singletons to learn more.
I have a tabbed view controller in my application and I use the same array for multiple tab views. I accomplish this by declaring the array outside of any classes (in the lines between import UIKit and the class declaration) so that it is essentially a global variable that every view can access. Have you tried this?
You can override the tabBar(didSelect:) method and then index the array of ViewControllers on the UITabViewController, and cast the ViewController to the desired Custom ViewController. No need for shared mutable state and all the problems that come with it.
class SecondViewController: UIViewController {
var array: [Int] = []
override func viewDidLoad() {
super.viewDidLoad()
}
}
class TabViewController: UITabBarController {
override func tabBar(
_ tabBar: UITabBar,
didSelect item: UITabBarItem
) {
super.tabBar(tabBar, didSelect: item)
var secondTab = viewControllers?[1] as? SecondViewController
secondTab?.array = [1, 2, 3]
}
}