Update View on button click SwiftUI - ios

noob here
I am creating a game in which you have to flip tiles and find pairs.
Now I am faced with the following problem: when I click on the "New Game" button, I need the tiles to be updated and filled with a new array, but this does not happen.
My assumption is that after clicking the button I need to update the view, but I could not figure out how to do this.
Help me please
struct ContentView: View {
#ObservedObject var viewModel: EmojiMemoryGame
var model: MemoryGameTheme
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 75))]) {
ForEach (viewModel.cards) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
viewModel.choose(card)
}
}
}
}
.foregroundColor(.red)
Spacer()
HStack {
newGame
Spacer()
nameTheme
}
}
.padding(.horizontal)
}
var newGame: some View {
Button(action: {
model.refreshTheme()
self.onActivate()
}, label: {
Text("New Game")
.font(.title)
.fontWeight(.heavy)
})
}
var nameTheme: some View {
Text ("\(MemoryGameTheme.themeName)")
.font(.title3)
.fontWeight(.heavy)
}
}
struct CardView: View {
let card: MemoryGame<String>.Card
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 20)
if card.isFaceUp {
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: 3)
Text(card.content).font(.largeTitle)
} else if card.isMatched {
shape.opacity(0)
} else {
shape.fill()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let game = EmojiMemoryGame()
let theme = MemoryGameTheme()
ContentView(viewModel: game, model: theme)
.preferredColorScheme(.dark)
ContentView(viewModel: game, model: theme)
.preferredColorScheme(.light)
}
}
import Foundation
import Accessibility
import SwiftUI
class MemoryGameTheme: ObservableObject {
enum ChoiseTheme: CaseIterable {
case car
case animal
case item
case food
case face
}
static var choiseTheme = ["๐Ÿฃ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ™ˆ", "๐Ÿต", "๐Ÿฝ", "๐Ÿธ", "๐ŸฆŠ", "๐Ÿจ", "๐Ÿน", "๐Ÿฐ", "๐Ÿป", "๐Ÿผ", "๐Ÿปโ€โ„๏ธ", "๐Ÿถ", "๐Ÿฑ", "๐Ÿฎ"]
static var themeName = "Animals"
static var colorCard = Color.gray
func theme(_ choiseTheme: ChoiseTheme) {
let randomTheme = ChoiseTheme.allCases.randomElement()!
switch randomTheme {
case .car:
let emojis = ["๐Ÿš—", "๐Ÿš•", "๐Ÿš™", "๐ŸšŒ", "๐ŸšŽ", "๐ŸŽ", "๐Ÿš“", "๐Ÿš‘", "๐Ÿš’", "๐Ÿš", "๐Ÿ›ป", "๐Ÿšš", "๐Ÿš›", "๐Ÿšœ", "๐Ÿ", "๐Ÿšฒ", "๐Ÿš", "โœˆ๏ธ", "๐Ÿ›ต", "๐Ÿš€", "๐Ÿ›ถ", "๐Ÿ›ธ", "โ›ต๏ธ", "๐Ÿš…"] // red
MemoryGameTheme.choiseTheme = emojis
MemoryGameTheme.themeName = "Cars"
MemoryGameTheme.colorCard = .red
case .animal:
let emojis = ["๐Ÿฃ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ™ˆ", "๐Ÿต", "๐Ÿฝ", "๐Ÿธ", "๐ŸฆŠ", "๐Ÿจ", "๐Ÿน", "๐Ÿฐ", "๐Ÿป", "๐Ÿผ", "๐Ÿปโ€โ„๏ธ", "๐Ÿถ", "๐Ÿฑ", "๐Ÿฎ"]
// gray
MemoryGameTheme.choiseTheme = emojis
MemoryGameTheme.themeName = "Animals"
MemoryGameTheme.colorCard = .gray
case .item:
let emojis = ["๐Ÿ’ป", "โŒš๏ธ", "๐Ÿ“ฑ", "โŒจ๏ธ", "๐Ÿ–ฅ", "๐Ÿ–จ", "๐Ÿ–ฒ", "๐Ÿ•น", "๐ŸŽฅ", "๐Ÿ“ท", "โ˜Ž๏ธ", "๐Ÿ“บ", "โฐ", "๐ŸŽ›", "๐Ÿ“Ÿ", "๐Ÿ“ก", "๐Ÿงฏ"]
// purple
MemoryGameTheme.choiseTheme = emojis
MemoryGameTheme.themeName = "Items"
MemoryGameTheme.colorCard = .purple
case .food:
let emojis = ["๐ŸŽ", "๐Ÿ", "๐ŸŠ", "๐Ÿ", "๐Ÿ‹", "๐ŸŒ", "๐Ÿฅญ", "๐Ÿ’", "๐Ÿ†", "๐Ÿ‘", "๐Ÿ“", "๐Ÿซ", "๐Ÿซ’", "๐Ÿง€", "๐ŸŸ", "๐Ÿ•", "๐Ÿฅ™"]
// yelow
MemoryGameTheme.choiseTheme = emojis
MemoryGameTheme.themeName = "Food"
MemoryGameTheme.colorCard = .yellow
case .face:
let emojis = ["๐Ÿ˜€", "๐Ÿฅณ", "๐Ÿ˜‹", "๐Ÿ˜—", "๐Ÿฅฐ", "๐Ÿ˜", "๐Ÿ˜›", "๐Ÿ˜Œ", "๐Ÿ˜ก", "๐Ÿคฏ", "๐Ÿคฌ", "๐Ÿฅต", "๐Ÿ˜ซ", "๐Ÿง", "โ˜บ๏ธ", "๐Ÿ˜ƒ", "๐Ÿ˜„", "๐Ÿ˜"] // green
MemoryGameTheme.choiseTheme = emojis
MemoryGameTheme.themeName = "Face"
MemoryGameTheme.colorCard = .green
}
print(randomTheme)
print(MemoryGameTheme.choiseTheme)
}
func refreshTheme() {
theme(ChoiseTheme.car)
}
}
import SwiftUI
class EmojiMemoryGame: ObservableObject {
static func createMemoreGame() -> MemoryGame<String> {
return MemoryGame<String>(numberOfPairOfCards: MemoryGameTheme.choiseTheme.count) { pairIndex in
MemoryGameTheme.choiseTheme[pairIndex]
}
}
#Published private var model: MemoryGame<String> = createMemoreGame()
var cards: Array<MemoryGame<String>.Card> {
model.cards
}
// MARK: - Intens(s)
func choose(_ card: MemoryGame<String>.Card) {
model.choose(card)
}
}
import Foundation
struct MemoryGame<CardContent> where CardContent: Equatable {
private(set) var cards: Array<Card>
private var indexOfTheOneAndOnlyFaceUpCard: Int?
mutating func choose(_ card: Card) {
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }),
!cards[chosenIndex].isFaceUp,
!cards[chosenIndex].isMatched {
if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard {
if cards[chosenIndex].content == cards[potentialMatchIndex].content {
cards[chosenIndex].isMatched = true
cards[potentialMatchIndex].isMatched = true
}
indexOfTheOneAndOnlyFaceUpCard = nil
} else {
for index in cards.indices {
cards[index].isFaceUp = false
}
indexOfTheOneAndOnlyFaceUpCard = chosenIndex
}
cards[chosenIndex].isFaceUp.toggle()
}
// print("\(cards)")
}
init (numberOfPairOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = Array<Card>()
// add numberOfPairOfCards x2 cards to cards array
for pairIndex in 0..<numberOfPairOfCards {
let content = createCardContent(pairIndex)
cards.append(Card(content: content, id: pairIndex*2))
cards.append(Card(content: content, id: pairIndex*2+1))
}
cards.shuffle()
}
struct Card: Identifiable {
var isFaceUp: Bool = false
var isMatched: Bool = false
var content: CardContent
var id: Int
}
}

model is not declared as StateObject, so any change in it will not be reflected in the UI.
EDIT : there is a logic problem in your code :
model is not used as a var but as a link to static class properties
the theme is choosen via static properties from MemoryGameClass
As you use static properties of a class, the values are only used when something that use this properties is updated, not when the property is updated.
The theme should be a published var of EmojisMemoryGame and declared as a struct. So when something changes in properties of the theme, it will cause a redraw of the view.
Edit 2 : what I meant :
Theme as a struct :
struct MemoryGameTheme {
// List of themes
static let allThemes: [ChoiseTheme: MemoryGameTheme] =
[.car: MemoryGameTheme(choiseTheme: .car, themeEmojis: ["๐Ÿš—", "๐Ÿš•", "๐Ÿš™", "๐ŸšŒ", "๐ŸšŽ", "๐ŸŽ", "๐Ÿš“", "๐Ÿš‘", "๐Ÿš’", "๐Ÿš", "๐Ÿ›ป", "๐Ÿšš", "๐Ÿš›", "๐Ÿšœ", "๐Ÿ", "๐Ÿšฒ", "๐Ÿš", "โœˆ๏ธ", "๐Ÿ›ต", "๐Ÿš€", "๐Ÿ›ถ", "๐Ÿ›ธ", "โ›ต๏ธ", "๐Ÿš…"], themeName: "Cars", colorCard: .red),
.animal: MemoryGameTheme(choiseTheme: .animal, themeEmojis: ["๐Ÿฃ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ™ˆ", "๐Ÿต", "๐Ÿฝ", "๐Ÿธ", "๐ŸฆŠ", "๐Ÿจ", "๐Ÿน", "๐Ÿฐ", "๐Ÿป", "๐Ÿผ", "๐Ÿปโ€โ„๏ธ", "๐Ÿถ", "๐Ÿฑ", "๐Ÿฎ"], themeName: "Animals", colorCard: .gray),
.item: MemoryGameTheme(choiseTheme: .item, themeEmojis: ["๐Ÿ’ป", "โŒš๏ธ", "๐Ÿ“ฑ", "โŒจ๏ธ", "๐Ÿ–ฅ", "๐Ÿ–จ", "๐Ÿ–ฒ", "๐Ÿ•น", "๐ŸŽฅ", "๐Ÿ“ท", "โ˜Ž๏ธ", "๐Ÿ“บ", "โฐ", "๐ŸŽ›", "๐Ÿ“Ÿ", "๐Ÿ“ก", "๐Ÿงฏ"], themeName: "Items", colorCard: .purple),
.food: MemoryGameTheme(choiseTheme: .food, themeEmojis: ["๐ŸŽ", "๐Ÿ", "๐ŸŠ", "๐Ÿ", "๐Ÿ‹", "๐ŸŒ", "๐Ÿฅญ", "๐Ÿ’", "๐Ÿ†", "๐Ÿ‘", "๐Ÿ“", "๐Ÿซ", "๐Ÿซ’", "๐Ÿง€", "๐ŸŸ", "๐Ÿ•", "๐Ÿฅ™"], themeName: "Food", colorCard: .yellow),
.face: MemoryGameTheme(choiseTheme: .face, themeEmojis: ["๐Ÿ˜€", "๐Ÿฅณ", "๐Ÿ˜‹", "๐Ÿ˜—", "๐Ÿฅฐ", "๐Ÿ˜", "๐Ÿ˜›", "๐Ÿ˜Œ", "๐Ÿ˜ก", "๐Ÿคฏ", "๐Ÿคฌ", "๐Ÿฅต", "๐Ÿ˜ซ", "๐Ÿง", "โ˜บ๏ธ", "๐Ÿ˜ƒ", "๐Ÿ˜„", "๐Ÿ˜"], themeName: "Face", colorCard: .green)]
// Type of themes
enum ChoiseTheme: CaseIterable {
case car
case animal
case item
case food
case face
}
var choiseTheme: ChoiseTheme = .animal
var themeEmojis = ["๐Ÿฃ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ™ˆ", "๐Ÿต", "๐Ÿฝ", "๐Ÿธ", "๐ŸฆŠ", "๐Ÿจ", "๐Ÿน", "๐Ÿฐ", "๐Ÿป", "๐Ÿผ", "๐Ÿปโ€โ„๏ธ", "๐Ÿถ", "๐Ÿฑ", "๐Ÿฎ"]
var themeName = "Animals"
var colorCard = Color.gray
mutating func changeTheme(_ choiseTheme: ChoiseTheme) {
let randomTheme = ChoiseTheme.allCases.randomElement()!
// Initialise the theme from static dictionnary
let theme = Self.allThemes[randomTheme]!
self.choiseTheme = theme.choiseTheme
self.themeEmojis = theme.themeEmojis
self.themeName = theme.themeName
self.colorCard = theme.colorCard
print(randomTheme)
print(choiseTheme)
}
// Mutating func to change theme
mutating func refreshTheme() {
changeTheme(ChoiseTheme.car)
}
}
EmojiGame including theme as Published var :
class EmojiMemoryGame: ObservableObject {
// Create a new memory game based on specific theme
static func createMemoreGame(theme: MemoryGameTheme) -> MemoryGame<String> {
return MemoryGame<String>(numberOfPairOfCards: theme.themeEmojis.count) { pairIndex in
theme.themeEmojis[pairIndex]
}
}
// This model is a struct
#Published private var model: MemoryGame<String> = EmojiMemoryGame.createMemoreGame(theme: MemoryGameTheme())
// This is the theme of the model
#Published private var theme: MemoryGameTheme = MemoryGameTheme()
// To acces model and theme comtents
var themeName: String {
theme.themeName
}
var colorCard: Color {
theme.colorCard
}
var cards: Array<MemoryGame<String>.Card> {
model.cards
}
// MARK: - Intens(s)
func choose(_ card: MemoryGame<String>.Card) {
model.choose(card)
}
// When refreshing theme, create a new game with teh new theme
func refreshTheme() {
theme.refreshTheme()
model = Self.createMemoreGame(theme: theme)
}
}
The Content view using ObservedObject Emoji game :
struct ContentView: View {
// emoji memory game must declared as Observed object
// to enable SwiftUI to update when something change in it
#ObservedObject var emojisGame: EmojiMemoryGame
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 75))]) {
ForEach (emojisGame.cards) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
emojisGame.choose(card)
}
}
}
}
// Use theme color for cards
.foregroundColor(emojisGame.colorCard)
Spacer()
HStack {
newGame
Spacer()
nameTheme
}
}
.padding(.horizontal)
}
var newGame: some View {
Button(action: {
// Reset emoji game with theme change
emojisGame.refreshTheme()
self.onActivate()
}, label: {
Text("New Game")
.font(.title)
.fontWeight(.heavy)
})
}
var nameTheme: some View {
Text ("\(emojisGame.themeName)")
.font(.title3)
.fontWeight(.heavy)
}
func onActivate() {
}
}

Related

SwiftUI: Running into issues with Drag and Drop inside a LazyVGrid

I've been working on my own smart home app and have run into some issues when trying to build the grid for the app.
I've been basing this home app on this tutorial. The goal is that one can reorder the individually sized blocks in the grid basically like he or she wants. The blocks(items) represent different gadgets in the smart home application. The issue I'm facing is that I can't seem to get the drag & drop to work. Maybe it's better to put all the item views in one custom view and then run a "ForEach" loop for them so that the .onDrag works? I'm relatively new to SwiftUI so I appreciate every hint I can get this program to work.
Here is my code:
ItemModel1:
struct ItemModel: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel {
return ItemModel(id: id, title: title)
}
}
ItemModel2:
struct ItemModel2: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel2 {
return ItemModel2(id: id, title: title)
}
}
It's essentially the same for the two other ItemModels 3 and 4..
ItemViewModel:
class ItemViewModel {
var items: [ItemModel] = []
#Published var currentGrid: ItemModel?
init() {
getItems()
}
func getItems() {
let newItems = [
ItemModel(title: "Item1"),
]
items.append(contentsOf: newItems)
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
ContentView:
struct DropViewDelegate: DropDelegate {
var grid: ItemModel
var gridData: ItemViewModel
func performDrop(info: DropInfo) -> Bool {
return true
}
func dropEntered(info: DropInfo) {
let fromIndex = gridData.items.firstIndex { (grid) -> Bool in
return self.grid.id == gridData.currentGrid?.id
} ?? 0
let toIndex = gridData.items.firstIndex { (grid) -> Bool in
return self.grid.id == self.grid.id
} ?? 0
if fromIndex != toIndex{
withAnimation(.default){
let fromGrid = gridData.items[fromIndex]
gridData.items[fromIndex] = gridData.items[toIndex]
gridData.items[toIndex] = fromGrid
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
}
struct ContentView: View {
#State var items: [ItemModel] = []
#State var items2: [ItemModel2] = []
#State var items3: [ItemModel3] = []
#State var items4: [ItemModel4] = []
#State var gridData = ItemViewModel()
let columns = [
GridItem(.adaptive(minimum: 160)),
GridItem(.adaptive(minimum: 160)),
]
let columns2 = [
GridItem(.flexible()),
]
var body: some View {
ZStack{
ScrollView{
VStack{
HStack(alignment: .top){
Button(action: saveButtonPressed, label: {
Text("Item1")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed2, label: {
Text("Item2")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed3, label: {
Text("Item3")
.font(.title2)
.foregroundColor(.white)
})
Button(action: saveButtonPressed4, label: {
Text("Item4")
.font(.title2)
.foregroundColor(.white)
})
}
LazyVGrid(
columns: columns,
alignment: .leading,
spacing: 12
){
ForEach(items) { item in
Item1View (item: item)
if 1 == 1 { Color.clear }
}
ForEach(items4) { item4 in
Item4View (item4: item4)
if 1 == 1 { Color.clear }
}
ForEach(items2) { item2 in
Item2View (item2: item2)
}
LazyVGrid(
columns: columns2,
alignment: .leading,
spacing: 12
){
ForEach(items3) { item3 in
Item3View (item3: item3)
}
}
}
.onDrag({
self.gridData = items
return NSItemProvider(item: nil, typeIdentifier:
self.grid)
})
.onDrop(of: [items], delegate: DropViewDelegate(grid:
items, gridData: gridData))
}
}
}
}
func saveButtonPressed() {
addItem(title: "Hello")
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func saveButtonPressed2() {
addItem2(title: "Hello")
}
func addItem2(title: String) {
let newItem = ItemModel2(title: title)
items2.append(newItem)
}
func saveButtonPressed3() {
addItem3(title: "Hello")
}
func addItem3(title: String) {
let newItem = ItemModel3(title: title)
items3.append(newItem)
}
func saveButtonPressed4() {
addItem4(title: "Hello")
}
func addItem4(title: String) {
let newItem = ItemModel4(title: title)
items4.append(newItem)
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
Item1:
struct Item1View: View {
#State var item: ItemModel
var body: some View {
HStack {
Text(item.title)
}
.padding( )
.frame(width: 396, height: 56)
.background(.black)
.cornerRadius(12.0)
}
}
Item2:
struct Item2View: View {
#State var item2: ItemModel2
var body: some View {
HStack {
Text(item2.title)
}
.padding( )
.frame(width: 182, height: 132)
.background(.black)
.cornerRadius(12.0)
}
}
Item3:
struct Item3View: View {
#State var item3: ItemModel3
var body: some View {
HStack {
Text(item3.title)
}
.padding( )
.frame(width: 182, height: 62)
.background(.black)
.cornerRadius(12.0)
}
}
Item4:
struct Item4View: View {
#State var item4: ItemModel4
var body: some View {
HStack {
Text(item4.title)
}
.padding( )
.frame(width: 396, height: 156)
.background(.black)
.cornerRadius(12.0)
}
}
I tried recreating the grid Asperi linked. However, the .onDrop doesn't seem to work like it should. The drop only occurs after you pressed another item to drag it. Only then will the previous items reorder themselves..
My version:
import SwiftUI
import UniformTypeIdentifiers
struct ItemModel6: Identifiable, Equatable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel6 {
return ItemModel6(id: id, title: title)
}
}
class Model: ObservableObject {
var data: [ItemModel6] = []
let columns = [
GridItem(.adaptive(minimum: 160)),
GridItem(.adaptive(minimum: 160)),
]
init() {
data = Array(repeating: ItemModel6(title: "title"), count:
100)
for i in 0..<data.count {
data[i] = ItemModel6(title: "Hello")
}
}
}
struct DemoDragRelocateView: View {
#StateObject private var model = Model()
#State private var dragging: ItemModel6?
var body: some View {
ScrollView {
LazyVGrid(columns: model.columns) {
ForEach(model.data) { item2 in GridItemView (item2:
item2)
.overlay(dragging?.id == item2.id ?
Color.white.opacity(0.8) : Color.clear)
.onDrag {
self.dragging = item2
return NSItemProvider(object:
String(item2.id) as NSString)
}
.onDrop(of: [UTType.text], delegate:
DragRelocateDelegate(item: item2, listData: $model.data,
current: $dragging))
}
}.animation(.default, value: model.data)
}
.onDrop(of: [UTType.text], delegate:
DropOutsideDelegate(current: $dragging))
}
}
struct DropOutsideDelegate: DropDelegate {
#Binding var current: ItemModel6?
func performDrop(info: DropInfo) -> Bool {
current = nil
return true
}
}
struct DragRelocateDelegate: DropDelegate {
let item: ItemModel6
#Binding var listData: [ItemModel6]
#Binding var current: ItemModel6?
func dropEntered(info: DropInfo) {
if item != current {
let from = listData.firstIndex(of: current!)!
let to = listData.firstIndex(of: item)!
if listData[to].id != current!.id {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
self.current = nil
return true
}
}
struct GridItemView: View {
#State var item2: ItemModel6
var body: some View {
HStack {
Text(item2.title)
}
.padding( )
.frame(width: 182, height: 132)
.background(.gray)
.cornerRadius(12.0)
}
}
struct DemoDragRelocateView_Previews: PreviewProvider {
static var previews: some View {
DemoDragRelocateView()
.preferredColorScheme(.dark)
}
}

ObservableObject not updating view in nested loop SWIFTUI

Regarding the following project :
You have an amountSum of 100
When you click on one user "plus" button, this specific user have to pay this amount but if you click on multiple user "plus" button, the amount to pay is divided between them equally.
Any idea how I can update the entire Model2.MustPayM2 prop when I click on the "plus" button please ?
import SwiftUI
struct Model1: Identifiable, Codable {
var id: String = UUID().uuidString
var nameM1: String
var amountM1: Double
var amountSumM1: Double = 100
var arrayM2: [Model2]
var isVisible: Bool = false
}
struct Model2: Identifiable, Codable {
var id: String = UUID().uuidString
var nameM2: String
var amountM2: Double = 0
var mustPayM2: Bool = false
}
class ViewModel1: ObservableObject {
#Published var Publi1: Model1
#Published var Publi1s: [Model1] = []
#Published var Publi2: Model2
#Published var Publi2s: [Model2] = []
init() {
let pub2 = Model2(nameM2: "init")
let pub1 = Model1(nameM1: "init", amountM1: 0, arrayM2: [pub2])
self.Publi2 = pub2
self.Publi1 = pub1
var newPub1s: [Model1] = []
for i in (0..<5) {
let newNameM1 = "name\(i+1)"
let newAmountM1 = Double(i+1)
var newModel1 = Model1(nameM1: newNameM1, amountM1: newAmountM1, arrayM2: [pub2])
var newPub2s: [Model2] = []
for i in (0..<5) {
let newNameM2 = "\(newNameM1)-user\(i+1)"
let newModel2 = Model2(nameM2: newNameM2)
newPub2s.append(newModel2)
}
newModel1.arrayM2 = newPub2s
newPub1s.append(newModel1)
}
Publi1s = newPub1s
Publi1 = newPub1s[0]
Publi2s = newPub1s[0].arrayM2
Publi2 = newPub1s[0].arrayM2[0]
}
}
struct View1: View {
#EnvironmentObject var VM1: ViewModel1
#State private var tt: String = ""
private let screenHeight = UIScreen.main.bounds.height
var body: some View {
ZStack {
VStack {
ForEach(0..<VM1.Publi2s.count, id: \.self) { i in
Text("\(VM1.Publi2s[i].nameM2)")
Text(tt)
Button {
VM1.Publi2s[i].mustPayM2.toggle()
var a = VM1.Publi2s.filter { $0.mustPayM2 == true }
let b = VM1.Publi1.amountM1 / Double(a.count)
// How can I update the new props between all users ??
// for j in 0..<a.count {
// a[j].amountM2 = b
// }
} label: {
Image(systemName: "plus")
}
}
Spacer()
Button {
VM1.Publi1.isVisible.toggle()
} label: {
Text("SHOW ME")
}
Spacer()
}
View2()
.offset(y: VM1.Publi1.isVisible ? 0 : screenHeight)
}
}
}
struct View2: View {
#EnvironmentObject var VM1: ViewModel1
var body: some View {
VStack {
Spacer()
ForEach(0..<VM1.Publi2s.count, id: \.self) { i in
Text("\(VM1.Publi2s[i].amountM2)")
}
}
}
}
struct View2_Previews: PreviewProvider {
static var previews: some View {
Group {
View1()
}
.environmentObject(ViewModel1())
}
}
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")
}
}
}
}

How does SwiftUI know which view's needed to redraw or not?

I have a question about process that SwiftUI does.
This code below is from a Stanford cs193p lecture.
// View
struct ContentView: View {
#ObservedObject var viewModel: EmojiMemoryGame
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65, maximum: 100))]) {
// Set break point 1
ForEach(viewModel.cards) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
viewModel.choose(card)
}
}
}
}
.foregroundColor(.red)
.padding(.horizontal)
}
}
struct CardView: View {
let card: MemoryGame<String>.Card
var body: some View {
// Set break point 2
ZStack {
let shape = RoundedRectangle(cornerRadius: 20)
if card.isFaceUp {
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: 3)
Text(card.content).font(.largeTitle)
} else if card.isMatched {
shape.opacity(0)
} else {
shape.fill()
}
}
}
}
// ViewModel
class EmojiMemoryGame: ObservableObject {
#Published private var model: MemoryGame<String> = EmojiMemoryGame.createMemoryGame()
static let emojis = ["๐Ÿ‘", "๐Ÿ–ฅ", "โ™ฅ๏ธ", "โ™ฆ๏ธ", "โ™ฃ๏ธ", "โ™ ๏ธ", "๐ŸŽ", "โŒ", "๐Ÿง‘๐Ÿปโ€๐Ÿ’ป", "๐Ÿ’ผ",
"รท", "โœ•", "โˆš", "๐Ÿ˜ญ", "โžก๏ธ", "๐Ÿ™Œ", "๐Ÿ†š", "๐Ÿ…ถ", "๐Ÿ“", "๐Ÿคœ"]
static func createMemoryGame() -> MemoryGame<String> {
return MemoryGame<String>(numberOfPairsOfCards: EmojiMemoryGame.emojis.count) { pairIndex in
EmojiMemoryGame.emojis[pairIndex]
}
}
var cards: [MemoryGame<String>.Card] {
return model.cards
}
func choose(_ card: MemoryGame<String>.Card) {
model.choose(card)
}
}
// Model
struct MemoryGame<CardContent: Equatable> {
private(set) var cards: [Card]
private var indexOfTheOneAndOnlyFaceUpCard: Int?
init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = []
for pairIndex in 0..<numberOfPairsOfCards {
let content = createCardContent(pairIndex)
cards.append(Card(content: content))
cards.append(Card(content: content))
}
}
mutating func choose(_ card: Card) {
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }),
!cards[chosenIndex].isFaceUp,
!cards[chosenIndex].isMatched
{
if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard {
if cards[chosenIndex].content == cards[potentialMatchIndex].content {
cards[chosenIndex].isMatched = true
cards[potentialMatchIndex].isMatched = true
}
indexOfTheOneAndOnlyFaceUpCard = nil
} else {
for index in cards.indices {
cards[index].isFaceUp = false
}
indexOfTheOneAndOnlyFaceUpCard = chosenIndex
}
cards[chosenIndex].isFaceUp.toggle()
}
}
struct Card: Identifiable {
let id: UUID = UUID()
var isFaceUp: Bool = false
var isMatched: Bool = false
var content: CardContent
}
}
As far as I know, viewModel.choose(card) is called, then some Card in cards(in MemoryGame) is changed. And next model(in EmojiMemoryGame) notices that I was changed, so finally ContentView will redraw the body(specifically some part that is depending on viewModel).
So I thought all CardView is going to be redrawn, but it's not. They are redrawing CardView only that has changed.
To double check, I set the break point like the comment in code above. It turns out, first break point came up for every card, but second break point just came up for just the card that had changed before.
Why does this happen like that?
Is it just SwiftUI's power?

Observable object model not changing View values when model is updated

I'm losing my mind over this, please help
I'm following the standford's iOS tutorial, I'm trying to finish an assignment of creating a card games, I have 3 models, Game, Card, Theme and Themes:
Game and Card are in charge of the main game logic
import Foundation
struct Game {
var cards: [Card]
var score = 0
var isGameOver = false
var theme: Theme
var choosenCardIndex: Int?
init(theme: Theme) {
cards = []
self.theme = theme
startTheme()
}
mutating func startTheme() {
cards = []
var contentItems: [String] = []
while contentItems.count != theme.numberOfPairs {
let randomElement = theme.emojis.randomElement()!
if !contentItems.contains(randomElement) {
contentItems.append(randomElement)
}
}
let secondContentItems: [String] = contentItems.shuffled()
for index in 0..<theme.numberOfPairs {
cards.append(Card(id: index*2, content: contentItems[index]))
cards.append(Card(id: index*2+1, content: secondContentItems[index]))
}
}
mutating func chooseCard(_ card: Card) {
print(card)
if let foundIndex = cards.firstIndex(where: {$0.id == card.id}),
!cards[foundIndex].isFaceUp,
!cards[foundIndex].isMatchedUp
{
if let potentialMatchIndex = choosenCardIndex {
if cards[foundIndex].content == cards[potentialMatchIndex].content {
cards[foundIndex].isMatchedUp = true
cards[potentialMatchIndex].isMatchedUp = true
}
choosenCardIndex = nil
} else {
for index in cards.indices {
cards[index].isFaceUp = false
}
}
cards[foundIndex].isFaceUp.toggle()
}
print(card)
}
mutating func endGame() {
isGameOver = true
}
mutating func penalizePoints() {
score -= 1
}
mutating func awardPoints () {
score += 2
}
struct Card: Identifiable, Equatable {
static func == (lhs: Game.Card, rhs: Game.Card) -> Bool {
return lhs.content == rhs.content
}
var id: Int
var isFaceUp: Bool = false
var content: String
var isMatchedUp: Bool = false
var isPreviouslySeen = false
}
}
Theme is for modeling different kind of content, Themes is for keeping track which one is currently in use and for fetching a new one
import Foundation
import SwiftUI
struct Theme: Equatable {
static func == (lhs: Theme, rhs: Theme) -> Bool {
return lhs.name == rhs.name
}
internal init(name: String, emojis: [String], numberOfPairs: Int, cardsColor: Color) {
self.name = name
self.emojis = Array(Set(emojis))
if(numberOfPairs > emojis.count || numberOfPairs < 1) {
self.numberOfPairs = emojis.count
} else {
self.numberOfPairs = numberOfPairs
}
self.cardsColor = cardsColor
}
var name: String
var emojis: [String]
var numberOfPairs: Int
var cardsColor: Color
}
import Foundation
struct Themes {
private let themes: [Theme]
public var currentTheme: Theme?
init(_ themes: [Theme]) {
self.themes = themes
self.currentTheme = getNewTheme()
}
private func getNewTheme() -> Theme {
let themesIndexes: [Int] = Array(0..<themes.count)
var visitedIndexes: [Int] = []
while(visitedIndexes.count < themesIndexes.count) {
let randomIndex = Int.random(in: 0..<themes.count)
let newTheme = themes[randomIndex]
if newTheme == currentTheme {
visitedIndexes.append(randomIndex)
} else {
return newTheme
}
}
return themes.randomElement()!
}
mutating func changeCurrentTheme() -> Theme {
self.currentTheme = getNewTheme()
return self.currentTheme!
}
}
This is my VM:
class GameViewModel: ObservableObject {
static let numbersTheme = Theme(name: "WeirdNumbers", emojis: ["1", "2", "4", "9", "20", "30"], numberOfPairs: 6, cardsColor: .pink)
static let emojisTheme = Theme(name: "Faces", emojis: ["๐Ÿฅฐ", "๐Ÿ˜„", "๐Ÿ˜œ", "๐Ÿฅณ", "๐Ÿค“", "๐Ÿ˜Ž", "๐Ÿ˜‹", "๐Ÿคฉ"], numberOfPairs: 8, cardsColor: .blue)
static let carsTheme = Theme(name: "Cars", emojis: ["๐Ÿš“", "๐ŸŽ๏ธ", "๐Ÿš—", "๐ŸšŽ", "๐Ÿš’", "๐Ÿš™", "๐Ÿš‘", "๐ŸšŒ"], numberOfPairs: 20, cardsColor: .yellow)
static let activitiesTheme = Theme(name: "Activities", emojis: ["๐Ÿคบ", "๐ŸŒ๏ธ", "๐Ÿ„โ€โ™‚๏ธ", "๐Ÿšฃ", "๐ŸŠโ€โ™‚๏ธ", "๐Ÿ‹๏ธ", "๐Ÿšดโ€โ™‚๏ธ"], numberOfPairs: -10, cardsColor: .green)
static let fruitsTheme = Theme(name: "Fruits", emojis: ["๐Ÿ‡", "๐Ÿ‰", "๐Ÿˆ", "๐ŸŠ", "๐Ÿ‹", "๐ŸŽ", "๐Ÿ", "๐Ÿฅญ"], numberOfPairs: 5, cardsColor: .purple)
static var themes = Themes([numbersTheme, emojisTheme, carsTheme, fruitsTheme])
static func createMemoryGame() -> Game {
Game(theme: themes.currentTheme!)
}
#Published private var gameController: Game = Game(theme: themes.currentTheme!)
func createNewGame() {
gameController.theme = GameViewModel.themes.changeCurrentTheme()
gameController.startTheme()
}
func choose(_ card: Game.Card) {
objectWillChange.send()
gameController.chooseCard(card)
}
var cards: [Game.Card] {
return gameController.cards
}
var title: String {
return gameController.theme.name
}
var color: Color {
return gameController.theme.cardsColor
}
}
And this is my view:
struct ContentView: View {
var columns: [GridItem] = [GridItem(.adaptive(minimum: 90, maximum: 400))]
#ObservedObject var ViewModel: GameViewModel
var body: some View {
VStack {
HStack {
Spacer()
Button(action: {
ViewModel.createNewGame()
}, label: {
VStack {
Image(systemName: "plus")
Text("New game")
.font(/*#START_MENU_TOKEN#*/.caption/*#END_MENU_TOKEN#*/)
}
})
.font(/*#START_MENU_TOKEN#*/.title/*#END_MENU_TOKEN#*/)
.padding(.trailing)
}
Section {
VStack {
Text(ViewModel.title)
.foregroundColor(/*#START_MENU_TOKEN#*/.blue/*#END_MENU_TOKEN#*/)
.font(/*#START_MENU_TOKEN#*/.title/*#END_MENU_TOKEN#*/)
}
}
ScrollView {
LazyVGrid(columns: columns ) {
ForEach(ViewModel.cards, id: \.id) { card in
Card(card: card, color: ViewModel.color)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
ViewModel.choose(card)
}
}
}
.font(.largeTitle)
}
.padding()
Text("Score")
.frame(maxWidth: .infinity, minHeight: 30)
.background(Color.blue)
.foregroundColor(/*#START_MENU_TOKEN#*/.white/*#END_MENU_TOKEN#*/)
Spacer()
HStack {
Spacer()
Text("0")
.font(.title2)
.bold()
Spacer()
}
}
}
}
struct Card: View {
let card: Game.Card
let color: Color
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 10)
if card.isFaceUp {
Text(card.content)
shape
.strokeBorder()
.accentColor(color)
.foregroundColor(color)
}
else {
shape
.fill(color)
}
}
}
}
Basically the problem lies with the
.onTapGesture {
ViewModel.choose(card)
}
Of the View, when someone taps a card, the isFaceUp property of the Card is changed to true, but this doesn't get reflected in the UI.
If I generate a new view by changing the theme and adding new cards, this works.
Button(action: {
ViewModel.createNewGame()
}, label: {
VStack {
Image(systemName: "plus")
Text("New game")
.font(/*#START_MENU_TOKEN#*/.caption/*#END_MENU_TOKEN#*/)
}
})
But when I'm trying to flip a card it doesn't work, the value changes in the Game model but it's not updated on the view
After the tap the ViewModel calls the choose method
func choose(_ card: Game.Card) {
gameController.chooseCard(card)
}
And this changed the value of the Model in the Game.swift file by calling the chooseCard method
mutating func chooseCard(_ card: Card) {
print(card)
if let foundIndex = cards.firstIndex(where: {$0.id == card.id}),
!cards[foundIndex].isFaceUp,
!cards[foundIndex].isMatchedUp
{
if let potentialMatchIndex = choosenCardIndex {
if cards[foundIndex].content == cards[potentialMatchIndex].content {
cards[foundIndex].isMatchedUp = true
cards[potentialMatchIndex].isMatchedUp = true
}
choosenCardIndex = nil
} else {
for index in cards.indices {
cards[index].isFaceUp = false
}
}
cards[foundIndex].isFaceUp.toggle()
}
print(card)
}
The values changes but the view does not, the gameController variable of the GameViewModel has the #Published state, which points to an instance of the Game model struct
#Published private var gameController: Game = Game(theme: themes.currentTheme!)
And the view it's accesing this GameViewModel with the #ObservedObject property
#ObservedObject var ViewModel: GameViewModel
I thought I was doing everything right, but I guess not lol, what the heck am I doing wrong? Why can't update my view if I'm using published and observable object on my ViewModel? lol
The main reason the card view doesn't see changes is because in your card view you did put an equatable conformance protocol where you specify an equality check == function that just checks for content and not other variable changes
static func ==(lhs: Game.Card, rhs: Game.Card) -> Bool {
lhs.content == rhs.content
// && lhs.isFaceUp && rhs.isFaceUp //<- you can still add this
}
if you remove the equatable protocol and leave swift to check for equality it should be the minimal change from your base solution.
I would still use the solution where you change the state of the class card so the view can react to changes as an ObservableObject, and the #Published for changes that the view need to track, like this:
class Card: Identifiable, Equatable, ObservableObject {
var id: Int
#Published var isFaceUp: Bool = false
var content: String
#Published var isMatchedUp: Bool = false
var isPreviouslySeen = false
internal init(id: Int, content: String) {
self.id = id
self.content = content
}
static func ==(lhs: Game.Card, rhs: Game.Card) -> Bool {
lhs.content == rhs.content
}
}
and in the Card view the card variable will become
struct Card: View {
#ObservedObject var card: Game.Card
...
}
btw you don't need to notify the view of changes with
objectWillChange.send() if you are already using the #Published notation. every set to the variable will trigger an update.
you could try this instead of declaring Card a class:
Card(card: card, color: ViewModel.color, isFaceUp: card.isFaceUp)
and add this to the Card view:
let isFaceUp: Bool
My understanding is that the Card view does not see any changes to the card (not sure why, maybe because it is in an if),
but if you give it something that has really changed then it is re-rendered. And as mentioned before no need for objectWillChange.send()
EDIT1:
you could also do this in "ContentView":
Card(viewModel: ViewModel, card: card)
and then
struct Card: View {
#ObservedObject var viewModel: GameViewModel
let card: Game.Card
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 10)
if card.isFaceUp {
Text(card.content)
shape
.strokeBorder()
.accentColor(viewModel.color)
.foregroundColor(viewModel.color)
}
else {
shape.fill(viewModel.color)
}
}
}
}

How to delete from dynamic range using only ForEach and not List

I can't find way to delete from a dynamic array that is being used in a ForEach loop. I've been looking with no luck. Many answers use List or don't have a binding in their ForEach. And I don't want to use list because it's hard to fully customize its design.
Below is a sample code that adds and remove elements from an array. This array is used to display a dynamic list of players.
Removing a player produces an index out of range after unwrapping the optional in ForEach loop.
import SwiftUI
struct GameRecapView: View {
#State private var game = Game(players: [Player(name: "Steph"),Player(name: "Kim")])
#State private var shape = Shape.circle
var body: some View {
VStack {
ForEach(self.game.players ,id: \.id) { player in
PlayerView(
player: self.$game.players[self.game.players.firstIndex(where: {$0.id == player.id})!],
shape: self.$shape)
}
Spacer()
Button(action: {
self.toggleShape()
}) {
Text("Change shape")
}
HStack {
Button(action: {
self.addPlayer(player: Player(name: "Eddye"))
}) {
Text("+")
}
Button(action: {
self.removeLastPlayer()
}) {
Text("-")
}
}
}
}
func toggleShape(){
if self.shape == .circle {
self.shape = .square
} else {
self.shape = .circle
}
}
func addPlayer(player : Player) {
self.game.players.append(player)
}
func removeLastPlayer(){
self.game.players.removeLast()
}
func removeItems(at offsets: IndexSet) {
self.game.players.remove(atOffsets: offsets)
}
}
struct PlayerView: View {
#Binding var player : Player
#Binding var shape : Shape
var letters = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t"]
var body: some View {
VStack {
ZStack{
Text(String(self.player.name.first!.uppercased()))
if shape == .square {
Rectangle().stroke().frame(width: 50, height: 50)
} else {
Circle().stroke().frame(width: 50, height: 50)
}
}
Button(action: {
self.player.name = self.letters.randomElement()!
}) {
Text("Change Name")
}
}
}
}
struct Game {
var players : [Player]
}
struct Player : Identifiable {
var id = UUID()
var name : String
}
enum Shape {
case
circle ,
square
}
struct GameRecapView_Previews: PreviewProvider {
static var previews: some View {
GameRecapView()
}
}
check this out: (tested and works)
always work on the single source of truth (as apple calls it) and not on copies. make sure your changes will be done by ObservableObject.
class Data : ObservableObject {
#Published var game = Game(players: [Player(name: "Steph"),Player(name: "Kim")])
}
struct GameRecapView: View {
#EnvironmentObject var data : Data
#State private var shape = Shape.circle
var body: some View {
VStack {
ForEach(self.data.game.players ,id: \.id) { player in
PlayerView(
player: self.data.game.players[self.data.game.players.firstIndex(where: {$0.id == player.id})!],
shape: self.$shape)
}
Spacer()
Button(action: {
self.toggleShape()
}) {
Text("Change shape")
}
HStack {
Button(action: {
self.addPlayer(player: Player(name: "Eddye"))
}) {
Text("+")
}
Button(action: {
self.removeLastPlayer()
}) {
Text("-")
}
}
}
}
func toggleShape(){
if self.shape == .circle {
self.shape = .square
} else {
self.shape = .circle
}
}
func addPlayer(player : Player) {
self.data.game.players.append(player)
}
func removeLastPlayer(){
self.data.game.players.removeLast()
}
func removeItems(at offsets: IndexSet) {
self.data.game.players.remove(atOffsets: offsets)
}
}
struct PlayerView: View {
#EnvironmentObject var data : Data
var player : Player
#Binding var shape : Shape
var letters = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t"]
var body: some View {
VStack {
ZStack{
Text(String(self.player.name.first!.uppercased()))
if shape == .square {
Rectangle().stroke().frame(width: 50, height: 50)
} else {
Circle().stroke().frame(width: 50, height: 50)
}
}
Button(action: {
let playerIndex = self.data.game.players.firstIndex(where: {$0.id == self.player.id})
self.data.game.players[playerIndex!].name = self.letters.randomElement()!
}) {
Text("Change Name")
}
}
}
}
struct Game {
var players : [Player]
}
struct Player : Identifiable {
var id = UUID()
var name : String
}
enum Shape {
case
circle ,
square
}
struct ContentView: View {
var body : some View {
Text("wtf")
}
}
struct GameRecapView_Previews: PreviewProvider {
static var previews: some View {
GameRecapView().environmentObject(Data())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Resources