How to tell SwiftUI views to bind to nested ObservableObjects
Nested models does not work yet in SwiftUI, but you could do something like this
class SubModel: ObservableObject {
@Published var count = 0
}
class AppModel: ObservableObject {
@Published var submodel: SubModel = SubModel()
var anyCancellable: AnyCancellable? = nil
init() {
anyCancellable = submodel.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}
}
}
Basically your AppModel
catches the event from SubModel
and send it further to the View
.
Edit:
If you do not need SubModel
to be class, then you could try something like this either:
struct SubModel{
var count = 0
}
class AppModel: ObservableObject {
@Published var submodel: SubModel = SubModel()
}
How to tell SwiftUI views to bind to more than one nested ObservableObject
You can expand upon the answer in the question you linked to by using CombineLatest
to have your model fire its objectWillChange
publisher when either of the underlying objects change:
import Combine
class Model: ObservableObject {
@Published var submodel1: Submodel1 = Submodel1()
@Published var submodel2: Submodel2 = Submodel2()
var anyCancellable: AnyCancellable? = nil
init() {
anyCancellable = Publishers.CombineLatest(submodel1.$count,submodel2.$count).sink(receiveValue: {_ in
self.objectWillChange.send()
})
}
}
SwiftUI Combine: Nested Observed-Objects
When a Ball
in the balls
array changes, you can call objectWillChange.send()
to update the ObservableObject
.
The follow should work for you:
class BallManager: ObservableObject {
@Published var balls: [Ball] {
didSet { setCancellables() }
}
let ballPublisher = PassthroughSubject()
private var cancellables = [AnyCancellable]()
init() {
self.balls = []
}
private func setCancellables() {
cancellables = balls.map { ball in
ball.objectWillChange.sink { [weak self] in
guard let self = self else { return }
self.objectWillChange.send()
self.ballPublisher.send(ball)
}
}
}
}
And get changes with:
.onReceive(bm.ballPublisher) { ball in
print("ball update:", ball.id, ball.color)
}
Note: If the initial value of balls
was passed in and not always an empty array, you should also call setCancellables()
in the init.
Updates changes for nested models
In your current code, menu
is only ever set once (on init
). You want to set menu
every time the object updates.
To do this, change the menu
after the settings
object has changed one of the values. Here we receive on the main
thread, then update the menu with setMenu()
.
You could also change the UserSettings
to use didSet
instead of willSet
, and then you will no longer require the .receive(on: DispatchQueue.main)
. However, this may be a bit counter-intuitive to the meaning of objectWillChange
.
Code:
class ViewModel: ObservableObject {
let settings = UserSettings()
@Published private(set) var menu: [Item] = []
private var cancellables: [AnyCancellable] = []
init() {
setMenu()
settings.objectWillChange.receive(on: DispatchQueue.main).sink { [unowned self] in
setMenu()
}
.store(in: &cancellables)
}
private func setMenu() {
menu = [
.item(Language(id: "a", enable: settings.itemA)),
.item(Language(id: "b", enable: settings.itemB)),
.item(Language(id: "c", enable: settings.itemC))
]
}
}
I did change some things slightly which would be unrelated, such as the access levels. You don't want the user to accidentally edit menu
directly.
ObservableObject not updating view in nested loop SWIFTUI
You implementation seems overly complicated and error prone. I´ve practically rewritten the code for this. I´ve added comments to make it clear what and why I have done certain things. If you don´t understand why, don´t hesitate to ask a question. But please read and try to understand the code first.
//Create one Model containing the individuals
struct Person: Identifiable, Codable{
var id = UUID()
var name: String
var amountToPay: Double = 0.0
var shouldPay: Bool = false
}
//Create one Viewmodel
class Viewmodel:ObservableObject{
//Entities being observed by the View
@Published var persons: [Person] = []
init(){
//Create data
persons = (0...4).map { index in
Person(name: "name \(index)")
}
}
//Function that can be called by the View to toggle the state
func togglePersonPay(with id: UUID){
let index = persons.firstIndex { $0.id == id}
guard let index = index else {
return
}
//Assign new value. This will trigger the UI to update
persons[index].shouldPay.toggle()
}
//Function to calculate the individual amount that should be paid and assign it
func calculatePayment(for amount: Double){
//Get all persons wich should pay
let personsToPay = persons.filter { $0.shouldPay }
//Calcualte the individual amount
let individualAmount = amount / Double(personsToPay.count)
//and assign it. This implementation will trigger the UI only once to update
persons = persons.map { person in
var person = person
person.amountToPay = person.shouldPay ? individualAmount : 0
return person
}
}
}
struct PersonView: View{
//pull the viewmodel from the environment
@EnvironmentObject private var viewmodel: Viewmodel
//The Entity that holds the individual data
var person: Person
var body: some View{
VStack{
HStack{
Text(person.name)
Text("\(person.amountToPay, specifier: "%.2f")$")
}
Button{
//toggle the state
viewmodel.togglePersonPay(with: person.id)
} label: {
//Assign label depending on person state
Image(systemName: "\(person.shouldPay ? "minus" : "plus")")
}
}
}
}
struct ContentView: View{
//Create and observe the viewmodel
@StateObject private var viewmodel = Viewmodel()
var body: some View{
VStack{
//Create loop to display person.
//Dont´t itterate over the indices this is bad practice
// itterate over the items themselves
ForEach(viewmodel.persons){ person in
PersonView(person: person )
.environmentObject(viewmodel)
.padding(10)
}
Button{
//call the func to calculate the result
viewmodel.calculatePayment(for: 100)
}label: {
Text("SHOW ME")
}
}
}
}
SwiftUI: matchedGeometryEffect With Nested Views
Here is an approach:
- Get the size of MainView with
GeometryReader
and pass it down. - in DetailView use
.overlay
which can grow bigger than its parent view,
if you specify an explicit.frame
- You need another inner
GeometryReader
to get the top pos of inner view for offset.
struct ContentView: View {
var body: some View {
// get size of overall view
GeometryReader { geo in
ScrollView(.vertical) {
Text("ShortCutView()")
PopularMoviesView(geo: geo)
}
}
}
}
struct PopularMoviesView: View {
// passed in geometry from parent view
var geo: GeometryProxy
// own view's top position, will be updated by GeometryReader further down
@State var ownTop = CGFloat.zero
@Namespace var namespace
@State var showDetails: Bool = false
@State var selectedMovie: Int?
var body: some View {
if !showDetails {
VStack {
HStack {
Text("Popular")
.font(.caption)
.padding()
Spacer()
Image(systemName: "arrow.forward")
.font(Font.title.weight(.medium))
.padding()
}
ScrollView(.horizontal) {
HStack {
ForEach(0..<10, id: \.self) { movie in
Text("MovieCell \(movie)")
.padding()
.matchedGeometryEffect(id: movie, in: namespace)
.frame(width: 200, height: 300)
.background(.yellow)
.onTapGesture {
self.selectedMovie = movie
withAnimation(Animation.interpolatingSpring(stiffness: 270, damping: 15)) {
showDetails.toggle()
}
}
}
}
}
}
}
if showDetails, let movie = selectedMovie {
// to get own view top pos
GeometryReader { geo in Color.clear.onAppear {
ownTop = geo.frame(in: .global).minY
print(ownTop)
}}
// overlay can become bigger than parent
.overlay (
Text("MovieDetail \(movie)")
.font(.largeTitle)
.matchedGeometryEffect(id: movie, in: namespace)
.frame(width: geo.size.width, height: geo.size.height)
.background(.gray)
.position(x: geo.frame(in: .global).midX, y: geo.frame(in: .global).midY - ownTop)
.onTapGesture {
withAnimation(Animation.interpolatingSpring(stiffness: 270, damping: 15)) {
showDetails.toggle()
}
}
)
}
}
}
Related Topics
Xcode 6: Keyboard Does Not Show Up in Simulator
Always Pass Weak Reference of Self into Block in Arc
How to Crop a Uiimageview to a New Uiimage in 'Aspect Fill' Mode
Watchkit Extension - No Matching Provisioning Profiles Found
Mkmapview: Instead of Annotation Pin, a Custom View
How to Compress/Resize Image on iOS Before Uploading to a Server
How to Get List of Available Bluetooth Devices
Missing Return Uitableviewcell
Correct Singleton Pattern Objective C (Ios)
How to Detect iPhone Is on Silent Mode
Difference Between Uiviewcontentmodescaleaspectfit and Uiviewcontentmodescaletofill
Event Handling For iOS - How Hittest:Withevent: and Pointinside:Withevent: Are Related
What Are Sprite Kit'S "Category Mask" and "Collision Mask"
Swift - How to Get the File Path Inside a Folder
How to Debug iOS 8 Extensions With Nslog
How to Make a Push Segue When a Uitableviewcell Is Selected