I have a number of SwapItem structs, each with a child SwapItemChild. Then, using a ForEach of SwiftUI, I would like to display the name of each SwapItem, called the item view, containing also a circle in the color of its respective SwapItemChild, called the child view. Subsequently, I would like to swap the children of two items, and have the respective child views change places animated. This was inspired by other examples of this effect by this extensive tutorial, but not specifically the children view swapping.
I attempt to do so using a matchedGeometryEffect identifying each child view by the id of the respective SwapItemChild. However, this leads to a jumpy animation, where only the top child view moves down, whereas the bottom child view instantaneously jumps to the top.
The functional example code is as follows.
// MARK: - Model
struct SwapItem: Identifiable {
let id = UUID()
let name: String
var child: SwapItemChild
}
struct SwapItemChild: Identifiable {
let id = UUID()
let color: Color
}
class SwapItemStore: ObservableObject {
#Published private(set) var items = [SwapItem(name: "Task 1", child: SwapItemChild(color: .red)),
SwapItem(name: "Task 2", child: SwapItemChild(color: .orange))]
func swapOuterChildren(){
let tmpChild = items[0].child
items[0].child = items[1].child
items[1].child = tmpChild
}
}
// MARK: - View
struct SwapTestView: View {
#StateObject private var swapItemStore = SwapItemStore()
#Namespace private var SwapViewNS
var body: some View {
VStack(spacing: 50.0) {
Button(action: swapItemStore.swapOuterChildren){
Text("Swap outer children")
.font(.title)
}
VStack(spacing: 150.0) {
ForEach(swapItemStore.items){ item in
SwapTestItemView(item: item, ns: SwapViewNS)
}
}
}
}
}
struct SwapTestItemView: View {
let item: SwapItem
let ns: Namespace.ID
var body: some View {
HStack {
Circle()
.fill(item.child.color)
.frame(width: 100, height: 100)
.matchedGeometryEffect(id: item.child.id, in: ns)
.animation(.spring())
Text(item.name)
}
}
}
What is the correct implementation of matchedGeometryEffect to have these child views swapping places seamlessly?
I have already encountered this kind of problem, try this :
ForEach(swapItemStore.items, id: \.self.child.id)
Another way :
struct SwapItem: Identifiable, Hashable {
let id = UUID()
let name: String
var child: SwapItemChild
}
struct SwapItemChild: Identifiable, Hashable {
let id = UUID()
let color: Color
}
with :
ForEach(swapItemStore.items, id: \.self)
See : https://www.hackingwithswift.com/books/ios-swiftui/why-does-self-work-for-foreach
Related
I have a state variable in an ObservedObject that determines which of two custom views I show in SwiftUI.
I've messed around with .animation(.easeIn) in various locations and tried things with .withAnimation(), but I can't get anything to happen besides XCode complaints while experimenting. Regardless of where I put .animation() I either get a compile error no nothing happens when I run the code. Just flick from one view to another when I trigger a state change.
struct EventEditorView : View { /* SwiftUI based View */
var eventEditorVC : EventEditorVC!
#ObservedObject var eventEditorDataModel: EventEditorDataModel
var body: some View {
switch( eventEditorDataModel.editMode) {
case .edit:
EventEditModeView(eventEditorVC: eventEditorVC, eventEditorDataModel: eventEditorDataModel)
case .view:
EventViewModeView(eventEditorVC: eventEditorVC, eventEditorDataModel: eventEditorDataModel)
}
}
}
You can use a .transition on your elements and withAnimation when you change the value that affects their state:
enum ViewToShow {
case one
case two
}
struct ContentView: View {
#State var viewToShow : ViewToShow = .one
var body: some View {
switch viewToShow {
case .one:
DetailView(title: "one", color: .red)
.transition(.opacity.combined(with: .move(edge: .leading)))
case .two:
DetailView(title: "two", color: .yellow)
.transition(.opacity.combined(with: .move(edge: .top)))
}
Button("Toggle") {
withAnimation {
viewToShow = viewToShow == .one ? .two : .one
}
}
}
}
struct DetailView : View {
var title: String
var color : Color
var body: some View {
Text(title)
.background(color)
}
}
I've been looking to add a loading indicator to my project, and found a really cool animation here. To make it easier to use, I wanted to incorporate it into a view modifier to put it on top of the current view. However, when I do so, it doesn't animate when I first press the button. I have played around with it a little, and my hypothesis is that the View Modifier doesn't pass the initial isAnimating = false, so only passes it isAnimating = true when the button is pressed. Because the ArcsAnimationView doesn't get the false value initially, it doesn't actually animate anything and just shows the static arcs. However, if I press the button a second time afterwards, it seems to be initialized and the view properly animates as desired.
Is there a better way to structure my code to avoid this issue? Am I missing something key? Any help is greatly appreciated.
Below is the complete code:
import SwiftUI
struct ArcsAnimationView: View {
#Binding var isAnimating: Bool
let count: UInt = 4
let width: CGFloat = 5
let spacing: CGFloat = 2
init(isAnimating: Binding<Bool>) {
self._isAnimating = isAnimating
}
var body: some View {
GeometryReader { geometry in
ForEach(0..<Int(count)) { index in
item(forIndex: index, in: geometry.size)
// the rotation below is what is animated ...
// I think the problem is that it just starts at .degrees(360), instead of
// .degrees(0) as expected, where it is then animated to .degrees(360)
.rotationEffect(isAnimating ? .degrees(360) : .degrees(0))
.animation(
Animation.default
.speed(Double.random(in: 0.05...0.25))
.repeatCount(isAnimating ? .max : 1, autoreverses: false)
, value: isAnimating
)
.foregroundColor(Color(hex: AppColors.darkBlue1.rawValue))
}
}
.aspectRatio(contentMode: .fit)
}
private func item(forIndex index: Int, in geometrySize: CGSize) -> some View {
Group { () -> Path in
var p = Path()
p.addArc(center: CGPoint(x: geometrySize.width/2, y: geometrySize.height/2),
radius: geometrySize.width/2 - width/2 - CGFloat(index) * (width + spacing),
startAngle: .degrees(0),
endAngle: .degrees(Double(Int.random(in: 120...300))),
clockwise: true)
return p.strokedPath(.init(lineWidth: width))
}
.frame(width: geometrySize.width, height: geometrySize.height)
}
}
struct ArcsAnimationModifier: ViewModifier {
#Binding var isAnimating: Bool
func body(content: Content) -> some View {
ZStack {
if isAnimating {
ArcsAnimationView(isAnimating: _isAnimating)
.frame(width: 150)
}
content
.disabled(isAnimating)
}
}
}
extension View {
func loadingAnimation(isAnimating: Binding<Bool>) -> some View {
self.modifier(ArcsAnimationModifier(isAnimating: isAnimating))
}
}
Here is where I actually call the function:
struct AnimationView: View {
#State var isAnimating = false
var body: some View {
VStack {
Button(action: {
self.isAnimating = true
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
self.isAnimating = false
}
}, label: {
Text("show animation")
})
}
.loadingAnimation(isAnimating: $isAnimating)
}
}
Note: I am fairly certain the issue is with View Modifier since if I call ArcsAnimationView as a regular view in AnimationView, it works as expected.
I get there to see some implementation, but I think others would prefer a simple base to start from.
here my 2 cents to show how to write an AnimatableModifier that can be used on multiple objects cleaning up ".animation" in code.
struct ContentView: View {
#State private var hideWhilelUpdating = false
var body: some View {
Image(systemName: "tshirt.fill")
.modifier(SmoothHideAndShow(hide: hideWhilelUpdating))
Text("Some contents to show...")
.modifier(SmoothHideAndShow(hide: hideWhilelUpdating))
Button( "hide and show smootly") {
hideWhilelUpdating.toggle()
}
.padding(60)
}
}
struct SmoothHideAndShow: AnimatableModifier {
var hide: Bool
var animatableData: CGFloat {
get { CGFloat(hide ? 0 : 1) }
set { hide = newValue == 0 }
}
func body(content: Content) -> some View {
content
.opacity(hide ? 0.2 : 1)
.animation(.easeIn(duration: 1), value: hide)
}
}
when pressing button, our bool will trigger animation that fades in and out our text.
I use it during network calls (omitted for clarity... and replaced with button) to hide values under remote update. When network returns, I toggle boolean.
I'm trying to deal groups of cards from a deck, and have the cards fly from the deck to their respective spots on screen (Stanford 2021 IOS class SET game). I'm tracking the status of each card, including whether it has been dealt or not. I thought I had all the logic correct, but the animation using a matchedGeometryEffect is not working. Both in the canvas view and when running the application in the simulator, the cards just go to their final spots in the overall View, and do not animate individually.
In an earlier version, I was seeing multiple debugger messages about "Multiple inserted views in matched geometry group Pair<Int, ID> have 'isSource: true'", but those messages no longer appear. With this slimmed-down version, the cards simply fail to animate between their original positions (in the card deck) and their final positions in the VGrid.
This codes uses a modified LazyVGrid to display the cards, with a control called AspectVGrid. The primary difference between the original LazyVGrid and the AspectVGrid is that the AspectVGrid creates a LazyVGrid with a fixed aspect ratio. The AspectVGrid only controls the grid layout itself. The code for that struct is included below.
When the application starts, all the cards are available in the deck, and each card's view is assigned its source matchedGeometryEffect. The cards are all marked as undealt (in a #State Set of card IDs). When the deck is tapped (onTapGesture), the model updates either twelve or three additional cards as wasDealt, and those newly dealt cards are supposed to be animated via a "withAnimation" code block, with individual Views in the AspectVGrid as their destinations.
Any suggestions on how to resolve this would be welcome. This seems like a pretty straightforward process, but I am clearly missing something in my implementation.
Thanks in advance for any ideas on this. I've included the model, View Model, and Views below.
Model
import Foundation
import SwiftUI
struct dataModel {
struct Card: Identifiable {
var wasDealt: Bool
var wasDiscarded: Bool
var dealDelay: Double
let id: Int
}
private(set) var firstCardWasDealt: Bool = false
private(set) var cards: Array<Card>
var cardsDealt: Array<Card> {
get {cards.filter({ card in card.wasDealt})}
}
private var numberOfCardsInPlay: Int {
get { cardsDealt.count }
}
var cardsDisplayed: Array<Card> {
get {cardsDealt.filter({ card in !card.wasDiscarded })}
}
var cardsInDeck: Array<Card> {
get {cards.filter({ card in !card.wasDealt })}
}
init() {
cards = []
newGame()
}
// Divides the total time to deal the cards by the number of cards to deal,
// and provides the applicable delay to the nth (index) card in the group of
// cards to be dealt.
private func calcDelay(numCards: Int, index: Int, totalDelay: Double) -> Double {
return Double(index) * (totalDelay / Double(numCards))
}
// If no cards have been dealt, deal twelve cards. Otherwise, deal three cards.
// When dealing the cards, apply the appropriate delay to each card being dealt.
// If all cards are already in play, don't deal any more cards.
mutating func dealMoreCards() {
if !firstCardWasDealt {
for index in (0...11) {
cards[index].dealDelay = calcDelay(numCards: 12, index: index, totalDelay: CardConstants.total12CardDealDuration)
cards[index].wasDealt = true
}
firstCardWasDealt = true
} else {
if numberOfCardsInPlay < cards.count {
let startIndex = numberOfCardsInPlay
for index in (startIndex...(startIndex + 2)) {
cards[index].dealDelay = calcDelay(numCards: 3, index: index, totalDelay: CardConstants.total3CardDealDuration)
cards[index].wasDealt = true
}
}
}
}
mutating func newGame() {
firstCardWasDealt = false
cards = []
for index in (0...80) {
cards.append(Card(wasDealt: false, wasDiscarded: false, dealDelay: 0, id: index))
}
}
}
struct CardConstants {
static let color = Color.red
static let aspectRatio: CGFloat = 2/3
static let dealAnimationDuration: Double = 0.2 // 0.5 - This value controls how long it takes to animate each card
static let total12CardDealDuration: Double = 3.0 // this controls how long it takes to deal twelve cards (in seconds)
static let total3CardDealDuration: Double = 0.75 // this controls how long it takes to deal three cards (in seconds)
}
View Model
import SwiftUI
class ViewModel: ObservableObject {
#Published private var model: dataModel
init() {
model = dataModel()
}
var cardsDealt: Array<dataModel.Card> {
model.cardsDealt
}
var cardsDisplayed: Array<dataModel.Card> {
model.cardsDisplayed
}
var cardsInDeck: Array<dataModel.Card> {
model.cardsInDeck
}
func choose(_ card: dataModel.Card) {
// do something with the chosen card
}
func dealMoreCards() {
model.dealMoreCards()
}
}
View
import SwiftUI
struct exampleView: View {
#ObservedObject var example: ViewModel
#Namespace private var dealingNamespace
#State private var dealt = Set<Int>()
private func deal(_ card: dataModel.Card) {
dealt.insert(card.id)
}
private func isUnDealt(_ card: dataModel.Card) -> Bool {
!dealt.contains(card.id)
}
var body: some View {
VStack {
Text("Example - \(example.cardsDisplayed.count) cards Displayed")
AspectVGrid(items: example.cardsDisplayed, aspectRatio: 2/3, content: { card in CardView(card: card)
.padding(4)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
.transition(AnyTransition.asymmetric(insertion: .opacity, removal: .opacity))
.onTapGesture {
example.choose(card)
}
})
HStack {
deckBody
Spacer()
}
}
}
var deckBody: some View {
ZStack {
ForEach(example.cardsInDeck) {
card in CardView(card: card)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
.transition(AnyTransition.asymmetric(insertion: .opacity, removal: .identity))
}
}
.frame(width: 60, height: 90)
.foregroundColor(.cyan)
.onTapGesture {
example.dealMoreCards() // Make the next group of cards available (12 or 3)
// deal cards
for card in example.cardsDealt {
if isUnDealt(card) {
withAnimation(Animation.easeInOut(duration: CardConstants.dealAnimationDuration).delay(card.dealDelay)) {
deal(card)
}
}
}
}
}
}
struct CardView: View {
let card: dataModel.Card
var body: some View {
VStack {
ZStack {
let shape = RoundedRectangle(cornerRadius: 5)
if card.wasDealt {
shape.fill().foregroundColor(.white)
} else {
shape.fill().foregroundColor(.cyan)
}
shape.strokeBorder(lineWidth: 3)
Text("Card: \(card.id)")
.padding(4)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let example = ViewModel()
exampleView(example: example)
}
}
AspectVGrid
//
// AspectVGrid.swift
//
// Created by CS193p Instructor on 4/14/21.
// Copyright Stanford University 2021
//
import SwiftUI
struct AspectVGrid<Item, ItemView>: View where ItemView: View, Item: Identifiable {
var items: [Item]
var aspectRatio: CGFloat
var content: (Item) -> ItemView
init(items: [Item], aspectRatio: CGFloat, #ViewBuilder content: #escaping (Item) -> ItemView) {
self.items = items
self.aspectRatio = aspectRatio
self.content = content
}
var body: some View {
GeometryReader { geometry in
VStack {
let width: CGFloat = widthThatFits(itemCount: items.count, in: geometry.size, itemAspectRatio: aspectRatio)
LazyVGrid(columns: [adaptiveGridItem(width: width)], spacing: 0) {
ForEach(items) { item in
content(item).aspectRatio(aspectRatio, contentMode: .fit)
}
}
Spacer(minLength: 0)
}
}
}
private func adaptiveGridItem(width: CGFloat) -> GridItem {
var gridItem = GridItem(.adaptive(minimum: width))
gridItem.spacing = 0
return gridItem
}
private func widthThatFits(itemCount: Int, in size: CGSize, itemAspectRatio: CGFloat) -> CGFloat {
var columnCount = 1
var rowCount = itemCount
repeat {
let itemWidth = size.width / CGFloat(columnCount)
let itemHeight = itemWidth / itemAspectRatio
if CGFloat(rowCount) * itemHeight < size.height {
break
}
columnCount += 1
rowCount = (itemCount + (columnCount - 1)) / columnCount
} while columnCount < itemCount
if columnCount > itemCount {
columnCount = itemCount
}
return floor(size.width / CGFloat(columnCount))
}
}
MatchedGeometryEffectApp
import SwiftUI
#main
struct MatchedGeometryEffectApp: App {
private let example = ViewModel()
var body: some Scene {
WindowGroup {
exampleView(example: example)
}
}
}
So, the problem you have been having with the matched geometry seems to be that AspectVGrid, which you were given as part of the assignment, interferes with the matched geometry animation. I suspect it has to do with the resizing mechanism. Removing that gives you a matched geometry that does what one would expect.
Here is a working example of the matched geometry in your ExampleView. I also rewrote CardView to make it simpler.
struct ExampleView: View {
#ObservedObject var example: ViewModel
#Namespace private var dealingNamespace
#State private var dealt = Set<Int>()
private func deal(_ card: DataModel.Card) {
dealt.insert(card.id)
}
private func isUnDealt(_ card: DataModel.Card) -> Bool {
!dealt.contains(card.id)
}
let columns = [
GridItem(.adaptive(minimum: 80))
]
var body: some View {
VStack {
VStack {
Text("Example - \(example.cardsDisplayed.count) cards Displayed")
LazyVGrid(columns: columns, spacing: 10) {
ForEach(example.cardsDisplayed) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
.onTapGesture {
example.choose(card)
}
.padding(4)
}
}
Spacer()
HStack {
deckBody
Spacer()
}
}
}
}
var deckBody: some View {
ZStack {
ForEach(example.cardsInDeck) { card in
CardView(card: card)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
}
}
.frame(width: 60, height: 90)
.foregroundColor(.cyan)
.onTapGesture {
withAnimation {
example.dealMoreCards() // Make the next group of cards available (12 or 3)
// deal cards
for card in example.cardsDealt {
if isUnDealt(card) {
deal(card)
}
}
}
}
}
}
struct CardView: View {
let card: DataModel.Card
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill()
.foregroundColor(card.wasDealt ? .white : .cyan)
RoundedRectangle(cornerRadius: 5)
.strokeBorder(lineWidth: 3)
Text("Card: \(card.id)")
.padding(4)
}
}
}
A quick note about naming conventions. Types and structs should all be capitalized, so ExampleView and DataModel would be convention, and I changed the code to reflect that. You will need to fix yours when you incorporate this code.
Also note that this view isn't perfect. Eventually, it will push the deck of cards off the bottom of the screen, but that can be handled.
Your attempt to make the cards fly one at a time doesn't work. First, the withAnimation has to capture example.dealMoreCards() and trying to implement a .delay in that situation doesn't work. It can be implemented with a DispatchQueue.main.asyncAfter(deadline:), but you would have to rework how you pass the timing as the individual cards won't be available yet.
Lastly, the transitions do nothing in this situation.
Working with SwiftUI:
I have a list of views in a ScrollView that I am creating using a ForEach loop. I want to show or hide a number of little flags depending on 4 different Bool properties in the struct I am using as the model for the objects in the list. The problem I'm having is the more If-statements I add, the worse the performance gets. With no If-statements the list loads without a hitch. I'm running into this problem with only 120 items in the list. I would love help figuring out what I'm doing wrong!
Here is an example of the Content View with the ScrollView and loop:
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
ScrollView(.vertical, showsIndicators: true){
VStack(spacing: 10){
ForEach(model.list){ item in
ItemView(item: item)
}
}
}
}// end body
} // end struct
And the Item Views and Struct I'm using as the model.
struct ListData: Identifiable {
var id = UUID()
var title: String
var subtitle: String
var isTrue: Bool
var isAlsoTrue:Bool
}
struct ItemView: View {
var item: ListData
var body: some View {
VStack(alignment: .leading){
Text(item.title)
HStack{
if item.isTrue{
FlagView(text: "True")
}
if item.isAlsoTrue{
FlagView(text: "Also")
}
Text(item.subtitle)
Spacer()
}
.font(.system(size: 12, weight: .semibold))
}
.font(.system(size: 14, weight: .semibold))
}
}
struct FlagView: View {
var text: String
var body: some View {
Text(text)
.padding(2)
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 2).foregroundColor(.gray))
}
}
The Actual objects have 4 Bool properties I want to use. Is there a better way to hide or show these flags in my list? Any help figuring out what's wrong with the performance is greatly appreciated!
I am trying to make individually moveable objects. I am able to successfully do it for one object but once I place it into an array, the objects are not able to move anymore.
Model:
class SocialStore: ObservableObject {
#Published var socials : [Social]
init(socials: [Social]){
self.socials = socials
}
}
class Social : ObservableObject{
var id: Int
var imageName: String
var companyName: String
#Published var pos: CGPoint
init(id: Int, imageName: String, companyName: String, pos: CGPoint) {
self.id = id
self.imageName = imageName
self.companyName = companyName
self.pos = pos
}
var dragGesture : some Gesture {
DragGesture()
.onChanged { value in
self.pos = value.location
print(self.pos)
}
}
}
Multiple image (images not following drag):
struct ContentView : View {
#ObservedObject var socialObject: SocialStore = SocialStore(socials: testData)
#ObservedObject var images: Social = testData[2]
var body: some View {
VStack {
ForEach(socialObject.socials, id: \.id) { social in
Image(social.imageName)
.position(social.pos)
.gesture(social.dragGesture)
}
}
}
}
Single image (image follow gesture):
struct ContentView : View {
#ObservedObject var socialObject: SocialStore = SocialStore(socials: testData)
#ObservedObject var images: Social = testData[2]
var body: some View {
VStack {
Image(images.imageName)
.position(images.pos)
.gesture(images.dragGesture)
}
}
}
I expect the individual items to be able to move freely . I see that the coordinates are updating but the position of each image is not.
First, a disclaimer: The code below is not meant as a copy-and-paste solution. Its only goal is to help you understand the challenge. There may be more efficient ways of resolving it, so take your time to think of your implementation once you understand the problem.
Why the view does not update?: The #Publisher in SocialStore will only emit an update when the array changes. Since nothing is being added or removed from the array, nothing will happen. Additionally, because the array elements are objects (and not values), when they do change their position, the array remains unaltered, because the reference to the objects remains the same. Remember: Classes create objects, Structs create values.
We need a way of making the store, to emit a change when something in its element changes. In the example below, your store will subscribe to each of its elements bindings. Now, all published updates from your items, will be relayed to your store publisher, and you will obtain the desired result.
import SwiftUI
import Combine
class SocialStore: ObservableObject {
#Published var socials : [Social]
var cancellables = [AnyCancellable]()
init(socials: [Social]){
self.socials = socials
self.socials.forEach({
let c = $0.objectWillChange.sink(receiveValue: { self.objectWillChange.send() })
// Important: You have to keep the returned value allocated,
// otherwise the sink subscription gets cancelled
self.cancellables.append(c)
})
}
}
class Social : ObservableObject{
var id: Int
var imageName: String
var companyName: String
#Published var pos: CGPoint
init(id: Int, imageName: String, companyName: String, pos: CGPoint) {
self.id = id
self.imageName = imageName
self.companyName = companyName
self.pos = pos
}
var dragGesture : some Gesture {
DragGesture()
.onChanged { value in
self.pos = value.location
print(self.pos)
}
}
}
struct ContentView : View {
#ObservedObject var socialObject: SocialStore = SocialStore(socials: testData)
var body: some View {
VStack {
ForEach(socialObject.socials, id: \.id) { social in
Image(social.imageName)
.position(social.pos)
.gesture(social.dragGesture)
}
}
}
}
For those who might find it helpful. This is a more generic approach to #kontiki 's answer.
This way you will not have to be repeating yourself for different model class types.
import Foundation
import Combine
import SwiftUI
class ObservableArray<T>: ObservableObject {
#Published var array:[T] = []
var cancellables = [AnyCancellable]()
init(array: [T]) {
self.array = array
}
func observeChildrenChanges<K>(_ type:K.Type) throws ->ObservableArray<T> where K : ObservableObject{
let array2 = array as! [K]
array2.forEach({
let c = $0.objectWillChange.sink(receiveValue: { _ in self.objectWillChange.send() })
// Important: You have to keep the returned value allocated,
// otherwise the sink subscription gets cancelled
self.cancellables.append(c)
})
return self
}
}
class Social : ObservableObject{
var id: Int
var imageName: String
var companyName: String
#Published var pos: CGPoint
init(id: Int, imageName: String, companyName: String, pos: CGPoint) {
self.id = id
self.imageName = imageName
self.companyName = companyName
self.pos = pos
}
var dragGesture : some Gesture {
DragGesture()
.onChanged { value in
self.pos = value.location
print(self.pos)
}
}
}
struct ContentView : View {
//For observing changes to the array only.
//No need for model class(in this case Social) to conform to ObservabeObject protocol
#ObservedObject var socialObject: ObservableArray<Social> = ObservableArray(array: testData)
//For observing changes to the array and changes inside its children
//Note: The model class(in this case Social) must conform to ObservableObject protocol
#ObservedObject var socialObject: ObservableArray<Social> = try! ObservableArray(array: testData).observeChildrenChanges(Social.self)
var body: some View {
VStack {
ForEach(socialObject.array, id: \.id) { social in
Image(social.imageName)
.position(social.pos)
.gesture(social.dragGesture)
}
}
}
}
There are two ObservableObject types and the one that you are interested in is Combine.ObservableObject. It requires an objectWillChange variable of type ObservableObjectPublisher and it is this that SwiftUI uses to trigger a new rendering. I am not sure what Foundation.ObservableObject is used for but it is confusing.
#Published creates a PassthroughSubject publisher that can be connected to a sink somewhere else but which isn't useful to SwiftUI, except for .onReceive() of course.
You need to implement
let objectWillChange = ObservableObjectPublisher()
in your ObservableObject class