In the code below, part of a crossword app, I have created a Grid, which contains squares, an #Published array of Squares, and a GridView to display an instance of Grid, griddy. When I change griddy's squares in the GridView, the GridView gets recreated, as expected, and I see an "!" instead of an "A".
Now I've added one more level -- a Puzzle contains an #Published Grid, griddy. In PuzzleView, I therefore work with puzzle.griddy. This doesn't work, though. The letter doesn't change, and a break point in PuzzleView's body never gets hit.
Why? I'm working on an app where I really need these 3 distinct structures.
import SwiftUI
class Square : ObservableObject {
#Published var letter: String
init(letter: String){
self.letter = letter
}
}
class Grid : ObservableObject {
#Published var squares:[[Square]] = [
[Square(letter: "A"), Square(letter: "B"), Square(letter: "C")],
[Square(letter: "D"), Square(letter: "E"), Square(letter: "F"), ]
]
}
class Puzzle: ObservableObject {
#Published var griddy: Grid = Grid()
}
struct GridView: View {
#EnvironmentObject var griddy: Grid
var body: some View {
VStack {
Text("\(griddy.squares[0][0].letter)")
Button("Change Numbers"){
griddy.squares[0][0] = Square(letter:"!")
}
}
}
}
struct PuzzleView: View {
#EnvironmentObject var puzzle: Puzzle
var body: some View {
VStack {
Text("\(puzzle.griddy.squares[0][0].letter)")
Button("Change Numbers"){
puzzle.griddy.squares[0][0] = Square(letter:"!")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
// PuzzleView().environmentObject(Puzzle())
GridView().environmentObject(Grid())
}
}
Technically you could just add this to your Puzzle object:
var anyCancellable: AnyCancellable? = nil
init() {
self.anyCancellable = griddy.objectWillChange.sink(receiveValue: {
self.objectWillChange.send()
})
}
It's a trick I found in another SO post, it'll cause changes in the child object to trigger the parent's objectWillChange.
Edit:
I would like to note that if you change the child object:
puzzle.griddy = someOtherGriddyObject
then you will still be subscribed to the changes of the old griddy object, and you won't receive new updates.
You can probably get by this by just updating the cancellable after changing the object:
puzzle.griddy = someOtherGriddyObject
puzzle.anyCancellable = puzzle.griddy.objectWillChange.sink(receiveValue: {
puzzle.objectWillChange.send()
})
Think in this direction: in MVVM SwiftUI concept every introduced ObservableObject entity must be paired with corresponding view ObservedObject (EnvironmentObject is the same as ObservedObject, just with different injection mechanism), so specific change in ViewModel would refresh corresponding View, there is no other magic/automatic propagations of changes from model to view.
So if Square is ObservableObject, there should be some SquareView, as
struct SquareView {
#ObservedObject var vm: Square
...
so if you Puzzle observable includes Grid observable, then corresponding PuzzleView having observed puzzle should include GridView having observed grid, which should include SqareView having observed square.
Ie.
struct PuzzleView: View {
#EnvironmentObject var puzzle: Puzzle
var body: some View {
GridView().environmentObject(puzzle.griddy)
}
}
// ... and same for GridView, and so on...
This approach result in very optimal update, because modifying one Square makes only one corresponding SquareView refreshed.
But if you accumulate all Squares.objectWillChange in one container update, then modifying one Square result in total UI update, that is bad and for UI fluency and for battery.
I'd say because Grid is reference type, and #Published is triggered when griddy is mutated.
class Puzzle: ObservableObject {
#Published var griddy: Grid = Grid()
}
Note that griddy does not actually mutate since it is a reference.
But why does it work in #EnvironmentObject var griddy: Grid?
My guess is that SDK implicitly observes publishers in an #ObservableObject.
But maybe someone can provide better detail answer.
I personally would avoid long publisher chain, and just flatten the nested structure.
Related
(You can skip this part and just look at the code.) I'm creating a complicated form. The form creates, say, a Post object, but I want to be able to create several Comment objects at the same time. So I have a Post form and a Comment form. In my Post form, I can fill out the title, description, etc., and I can add several Comment forms as I create more comments. Each form has an #ObservedObject viewModel of its own type. So I have one parent Post #ObservedObject viewModel, and another #ObservedObject viewModel for the array of the Comment objects which is also a #ObservedObject viewModel.
I hope that made some sense -- here is code to minimally reproduce the issue (unrelated to Posts/Comments). The objective is to make the count of the "Childish" viewModels at the parent level count up like how they count up for the "Child" view.
import Combine
import SwiftUI
final class ParentScreenViewModel: ObservableObject {
#Published var childScreenViewModel = ChildScreenViewModel()
}
struct ParentScreen: View {
#StateObject private var viewModel = ParentScreenViewModel()
var body: some View {
Form {
NavigationLink(destination: ChildScreen(viewModel: viewModel.childScreenViewModel)) {
Text("ChildishVMs")
Spacer()
Text("\(viewModel.childScreenViewModel.myViewModelArray.count)") // FIXME: this count is never updated
}
}
}
}
struct ParentScreen_Previews: PreviewProvider {
static var previews: some View {
ParentScreen()
}
}
// MARK: - ChildScreenViewModel
final class ChildScreenViewModel: ObservableObject {
#Published var myViewModelArray: [ChildishViewModel] = []
func appendAnObservedObject() {
objectWillChange.send() // FIXME: does not work
myViewModelArray.append(ChildishViewModel())
}
}
struct ChildScreen: View {
#ObservedObject private var viewModel: ChildScreenViewModel
init(viewModel: ChildScreenViewModel = ChildScreenViewModel()) {
self.viewModel = viewModel
}
var body: some View {
Button {
viewModel.appendAnObservedObject()
} label: {
Text("Append a ChildishVM (current num: \(viewModel.myViewModelArray.count))")
}
}
}
struct ChildScreen_Previews: PreviewProvider {
static var previews: some View {
ChildScreen()
}
}
final class ChildishViewModel: ObservableObject {
#Published var myProperty = "hey!"
}
ParentView:
ChildView:
I can't run this in previews either -- seems to need to be run in the simulator. There are lots of questions similar to this one but not quite like it (e.g. the common answer of manually subscribing to the child's changes using Combine does not work). Would using #EnvironmentObject help somehow? Thanks!
First get rid of the view model objects, we don't use those in SwiftUI. The View data struct is already the model for the actual views on screen e.g. UILabels, UITables etc. that SwiftUI updates for us. It takes advantage of value semantics to resolve consistency bugs you typically get with objects, see Choosing Between Structures and Classes. SwiftUI structs uses property wrappers like #State to make these super-fast structs have features like objects. If you use actual objects on top of the View structs then you are slowing down SwiftUI and re-introducing the consistency bugs that Swift and SwiftUI were designed to eliminate - which seems to me is exactly the problem you are facing. So it of course is not a good idea to use Combine to resolve consistency issues between objects it'll only make the problem worse.
So with that out of the way, you just need correct some mistakes in your design. Model types should be structs (these can be arrays or nested structs) and have a single model object to manage the life-cycle and side effects of the struct. You can have structs within structs and use bindings to pass them between your Views when you need write access, if you don't then its simply a let and SwiftUI will automatically call body whenever a View is init with a different let from last time.
Here is a basic example:
struct Post: Identifiable {
let id = UUID()
var text = ""
}
class Model: ObservableObject {
#Published var posts: [Post] = []
// func load
// func save
// func delete a post by ID
}
struct ModelController {
static let shared = ModelController()
let model = Model()
//static var preview: ModelController {
// ...
//}()
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(ModelController.shared.model)
}
}
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
ForEach($model.posts) { $post in
ContentView2(post: post)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(ModelController.shared.preview)
}
}
struct ConventView2: View {
#Binding var post: Post
var body: some View {
TextField("Enter Text", text: $post.text)
}
}
For a more detail check out Apple's Fruta and Scrumdinger samples.
From an architecture point of view, is it a good idea to use Published properties to control UI elements in a SwiftUI view hierarchy?
Consider the following problem,
I have a store as such
#MainActor final class SomeStore: ObservableObject {
#Published var shouldShowSuccessAlert: Boolean = false
#Published var someData: String = false
//Called by some networking library
public func networkRequestDidComplete(with data: String) {
self.someData = data
self.shouldShowSuccessAlert = false
}
}
//App
struct SomeApp: App {
#StateObject private var someStore: SomeStore = SomeStore()
var body: some Scene {
WindowGroup {
SomeView(shouldShowSuccessAlert: self.someStore.shouldShowSuccessAlert)
}
}
}
//View
struct SomeView: View {
#ObservedObject private var someStore: SomeStore
var body: some View {
if self.someStore.shouldShowSuccessAlert {
//some arbitrary alertView
AlertView(onComplete: {
self.someStore.shouldShowSuccessAlert = false
})
}
}
}
The only problem I can see is that changing this one Boolean will cause the entire view hierarchy to re-render. If SomeView has a deep view hierarchy, all of its descendants will be re-rendered. In React, for example, there is a concept of a PureComponent that you can use to prevent re-renders of views (called components in React) only if the properties they use change (by using reference equality checks) in order to improve performance. Is there something similar in SwiftUI?
Are there any other patterns that can be used like absorbing this boolean into the view's state and then controlled locally within the view?
I have this rather common situation where a List is displaying some items from an #ObservedObject data store as NavigationLinks.
On selecting a NavigationLink a DetailView is presented. This view has a simple Toggle conected to a #Published var on its ViewModel class.
When the DetailView appears (onAppear:) its View Model sets the published var that controls the Toggle to true and also triggers an async request that will update the main data store, causing the List in the previous screen to update too.
The problem is that when this happens (the List is reloaded from an action triggered in the Detail View) multiple instances of the DetailViewModel seem to retained and the DetailView starts receiving events from the wrong Publishers.
In the first time the Detail screen is reached the behaviour is correct (as shown in the code below), the toggle is set to true and the store is updated, however, on navigating back to the List and then again to another DetailView the toggle is set to true on appearing but this time when the code that reloads the store executes, the toggle is set back to false.
My understanding is that when the List is reloaded and a new DetailView and ViewModel are created (as the destination of the NavigationLink) the initial value from the isOn variable that controls the Toggle (which is false) is somehow triggering an update to the Toggle of the currently displayed screen.
Am I missing something here?
import SwiftUI
import Combine
class Store: ObservableObject {
static var shared: Store = .init()
#Published var items = ["one", "two"]
private init() { }
}
struct ContentView: View {
#ObservedObject var store = Store.shared
var body: some View {
NavigationView {
List(store.items, id: \.self) { item in
NavigationLink(item, destination: ItemDetailView())
}
}
}
}
struct ItemDetailView: View {
#ObservedObject var viewModel = ItemDetailViewModel()
var body: some View {
VStack {
Toggle("A toggle", isOn: $viewModel.isOn)
Text(viewModel.title)
Spacer()
} .onAppear(perform: viewModel.onAppear)
}
}
class ItemDetailViewModel: ObservableObject {
#ObservedObject var store: Store = .shared
#Published var isOn = false
var title: String {
store.items[0]
}
func onAppear() {
isOn = true
asyncOperationThatUpdatesTheStoreData()
}
private func asyncOperationThatUpdatesTheStoreData() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
self?.store.items = ["three", "four"]
}
}
}
You're controlling lifetimes and objects in a way that's not a pattern in this UI framework. The VieModel is not going to magically republish a singleton value type; it's going to instantiate with that value and then mutate its state without ever checking in with the shared instance again, unless it is rebuilt.
class Store: ObservableObject {
static var shared: Store = .init()
struct ContentView: View {
#ObservedObject var store = Store.shared
struct ItemDetailView: View {
#ObservedObject var viewModel = ItemDetailViewModel()
class ViewModel: ObservableObject {
#Published var items: [String] = Store.shared.items
There are lots of potential viable patterns. For example:
Create class RootStore: ObservableObject and house it as an #StateObject in App. The lifetime of a StateObject is the lifetime of that view hierarchy's lifetime. You can expose it (a) directly to child views by .environmentObject(store) or (b) create a container object, such as Factory that vends ViewModels and pass that via the environment without exposing the store to views, or (c) do both a and b.
If you reference the store in another class, hold a weak reference weak var store: Store?. If you keep state in that RootStore on #Published, then you can subscribe directly to that publisher or to the RootStore's own ObjectWillChangePublisher. You could also use other Combine publishers like CurrentValueSubject to achieve the same as #Published.
For code examples, see
Setting a StateObject value from child view causes NavigationView to pop all views
SwiftUI #Published Property Being Updated In DetailView
I have been reading about the property wrappers in SwiftUI and I see that they do a great job, but one thing which I really don't get is the difference between #EnvironmentObject and #ObservedObject.
From what I learned so far, I see that #EnvironmentObject is used when we have an object that is needed in various places in our app but we don't need to pass it through all of them. For example if we have hierarchy A -> B -> C -> D and the object is created at A, it is saved in the environment so that we can pass it directly from A to D, if D needs it.
If we use #ObservedObject which is created at A and needs to be passed to D, then we need to go through B and C as well.
But I still don't know how to decide which one to use.
Here are 2 example projects which I made:
struct ContentView2: View {
var order = Order2()
var body: some View {
VStack {
EditView2()
DisplayView2()
}
.environmentObject(order)
}
}
struct EditView2: View {
#EnvironmentObject var user: Order2
var body: some View {
HStack{
TextField("Fruit", text: $user.item)
}
}
}
struct DisplayView2: View {
#EnvironmentObject var user: Order2
var body: some View {
VStack{
Text(user.item)
}
}
}
class Order2: ObservableObject {
#Published var item = "Orange"
}
and
struct ContentView: View {
var order = Order()
var body: some View {
VStack {
EditView(order: order)
DisplayView(order: order)
}
}
}
struct EditView: View {
#ObservedObject var order: Order
var body: some View {
HStack{
TextField("Fruit", text: $order.item)
}
}
}
struct DisplayView: View {
#ObservedObject var order: Order
var body: some View {
VStack{
Text(order.item)
}
}
}
class Order: ObservableObject {
#Published var item = "Apple"
}
Both codes do the same update of the view. Also both ContentViews, pass an Order object. The difference is that Environment passes .environmentObject(order) and Observed passes it directly EditView(order: order). For me, both do same job, only their declaration is different, therefore I would appreciate some explanation or a better example.
As you've noticed an #ObservedObject needs to be passed from view to view. It may be better for a simple view hierarchy when you don't have too many views.
Let's assume you have the following hierarchy:
ViewA -> ViewB -> ViewC -> ViewD
Now if you want your #ObservedObject from the ViewA to be in the ViewB there's no problem with passing it directly in init.
But what if you want it in the ViewD as well? And what if you don't need it in the ViewB and ViewC?
With an #ObservedObject you'd need to manually pass it from the ViewA to the ViewB and then to the ViewC, and then to the ViewD. And you'd need to declare it in every child view.
With an #EnvironmentObject it's easy - just pass it to the top-level view:
ViewA().environmentObject(someObservableObject)
Then you only declare it in the view that uses it - this may make your code more readable.
Note
Every object in the environment (view hierarchy) can access the injected #EnvironmentObject. If you don't want this (privacy is important) you may need to pass it as an #ObservedObject instead.
I'm new to SwiftUI and understand that I may need to implement EnvironmentObject in some way, but I'm not sure how in this case.
This is the Trade class
class Trade {
var teamsSelected: [Team]
init(teamsSelected: [Team]) {
self.teamsSelected = teamsSelected
}
}
This is the child view. It has an instance trade from the Trade class. There is a button that appends 1 to array teamsSelected.
struct TeamRow: View {
var trade: Trade
var body: some View {
Button(action: {
self.trade.teamsSelected.append(1)
}) {
Text("Button")
}
}
}
This is the parent view. As you can see, I pass trade into the child view TeamRow. I want trade to be in sync with trade in TeamRow so that I can then pass trade.teamsSelected to TradeView.
struct TeamSelectView: View {
var trade = Trade(teamsSelected: [])
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: TradeView(teamsSelected: trade.teamsSelected)) {
Text("Trade")
}
List {
ForEach(teams) { team in
TeamRow(trade: self.trade)
}
}
}
}
}
}
I've taken your code and changed some things to illustrate how SwiftUI works in order to give you a better understanding of how to use ObservableObject, #ObservedObject, #State, and #Binding.
One thing to mention up front - #ObservedObject is currently broken when trying to run SwiftUI code on a physical device running iOS 13 Beta 6, 7, or 8. I answered a question here that goes into that in more detail and explains how to use #EnvironmentObject as a workaround.
Let's first take a look at Trade. Since you're looking to pass a Trade object between views, change properties on that Trade object, and then have those changes reflected in every view that uses that Trade object, you'll want to make Trade an ObservableObject. I've added an extra property to your Trade class purely for illustrative purposes that I'll explain later. I'm going to show you two ways to write an ObservableObject - the verbose way first to see how it works, and then the concise way.
import SwiftUI
import Combine
class Trade: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
var name: String {
willSet {
self.objectWillChange.send()
}
}
var teamsSelected: [Int] {
willSet {
self.objectWillChange.send()
}
}
init(name: String, teamsSelected: [Int]) {
self.name = name
self.teamsSelected = teamsSelected
}
}
When we conform to ObservableObject, we have the option to write our own ObservableObjectPublisher, which I've done by importing Combine and creating a PassthroughSubject. Then, when I want to publish that my object is about to change, I can call self.objectWillChange.send() as I have on willSet for name and teamsSelected.
This code can be shortened significantly, however. ObservableObject automatically synthesizes an object publisher, so we don't actually have to declare it ourselves. We can also use #Published to declare our properties that should send a publisher event instead of using self.objectWillChange.send() in willSet.
import SwiftUI
class Trade: ObservableObject {
#Published var name: String
#Published var teamsSelected: [Int]
init(name: String, teamsSelected: [Int]) {
self.name = name
self.teamsSelected = teamsSelected
}
}
Now let's take a look at your TeamSelectView, TeamRow, and TradeView. Keep in mind once again that I've made some changes (and added an example TradeView) just to illustrate a couple of things.
struct TeamSelectView: View {
#ObservedObject var trade = Trade(name: "Name", teamsSelected: [])
#State var teams = [1, 1, 1, 1, 1]
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: TradeView(trade: self.trade)) {
Text(self.trade.name)
}
List {
ForEach(self.teams, id: \.self) { team in
TeamRow(trade: self.trade)
}
}
Text("\(self.trade.teamsSelected.count)")
}
.navigationBarItems(trailing: Button("+", action: {
self.teams.append(1)
}))
}
}
}
struct TeamRow: View {
#ObservedObject var trade: Trade
var body: some View {
Button(action: {
self.trade.teamsSelected.append(1)
}) {
Text("Button")
}
}
}
struct TradeView: View {
#ObservedObject var trade: Trade
var body: some View {
VStack {
Text("\(self.trade.teamsSelected.count)")
TextField("Trade Name", text: self.$trade.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
}
}
Let's first look at #State var teams. We use #State for simple value types - Int, String, basic structs - or collections of simple value types. #ObservedObject is used for objects that conform to ObservableObject, which we use for data structures that are more complex than just Int or String.
What you'll notice with #State var teams is that I've added a navigation bar item that will append a new item to the teams array when pressed, and since our List is generated by iterating through that teams array, our view re-renders and adds a new item to our List whenever the button is pressed. That's a very basic example of how you would use #State.
Next, we have our #ObservedObject var trade. You'll notice that I'm not really doing anything different than you were originally. I'm still creating an instance of my Trade class and passing that instance between my views. But since it's now an ObservableObject, and we're using #ObservedObject, our views will now all receive publisher events whenever the Trade object changes and will automatically re-render their views to reflect those changes.
The last thing I want to point out is the TextField I created in TradeView to update the Trade object's name property.
TextField("Trade Name", text: self.$trade.name)
The $ character indicates that I'm passing a binding to the text field. This means that any changes TextField makes to name will be reflected in my Trade object. You can do the same thing yourself by declaring #Binding properties that allow you to pass bindings between views when you are trying to sync state between your views without passing entire objects.
While I changed your TradeView to take #ObservedObject var trade, you could simply pass teamsSelected to your trade view as a binding like this - TradeView(teamsSelected: self.$trade.teamsSelected) - as long as your TradeView accepts a binding. To configure your TradeView to accept a binding, all you would have to do is declare your teamsSelected property in TradeView like this:
#Binding var teamsSelected: [Team]
And lastly, if you run into issues with using #ObservedObject on a physical device, you can refer to my answer here for an explanation of how to use #EnvironmentObject as a workaround.
You can use #Binding and #State / #Published in Combine.
In other words, use a #Binding property in Child View and bind it with a #State or a #Published property in Parent View as following.
struct ChildView: View {
#Binding var property1: String
var body: some View {
VStack(alignment: .leading) {
TextField(placeholderTitle, text: $property1)
}
}
}
struct PrimaryTextField_Previews: PreviewProvider {
static var previews: some View {
PrimaryTextField(value: .constant(""))
}
}
struct ParentView: View{
#State linkedProperty: String = ""
//...
ChildView(property1: $linkedProperty)
//...
}
or if you have a #Publilshed property in your viewModel(#ObservedObject), then use it to bind the state like ChildView(property1: $viewModel.publishedProperty).
firstly thanks a lot to graycampbell for giving me a better understanding! However, my understanding does not seem to work out completely. I have a slightly different case which I'm not fully able to solve.
I've already asked my question in a separate thread, but I want to add it here as well, because it somehow fits the topic: Reading values from list of toggles in SwiftUI
Maybe somebody of you guys can help me with this. The main difference to the initial post if this topic is, that I have to collect Data from each GameGenerationRow in the GameGenerationView and then hand it over to another View.