I'm really new to swift. What am I doing wrong? Why isn't the image updating when pressing it? I couldn't find anything about state listeners.
This is the View that bundles two views:
import SwiftUI
struct FoodContentView: View {
var body: some View {
VStack {
imageView.gesture(tap)
FoodContentTextView().position(x: bodyWidth/3, y: bodyHeight/2)
}
}
}
var imageView = FoodContentImageView(selectedImage: "CheeseBurger")
let tap = TapGesture()
.onEnded { _ in
if imageView.imageSelected == "Burger" {
imageView.imageSelected = "CheeseBurger"
print(imageView.imageSelected)
} else {
imageView.imageSelected = "Burger"
print(imageView.imageSelected)
}
}
And this is the View where the image gets defined:
struct FoodContentImageView: View {
var imageSelected: String
init(selectedImage: String){
imageSelected = selectedImage
}
var body: some View {
Image(imageSelected).resizable().frame(width: 200, height: 200, alignment: .center).scaledToFit()
}
}
You are making it very complicated unnecessarily. FoodContentImageView just needs to know the name of the image:
struct FoodContentImageView: View {
var imageName: String
var body: some View {
Image(imageName).resizable().frame(width: 200, height: 200, alignment: .center).scaledToFit()
}
}
And you just need to pass in the name. If you store it as a #State, it will automatically updates on any change:
struct ContentView: View {
#State var text = "CheeseBurger"
var body: some View {
VStack {
FoodContentImageView(imageName: text)
.onTapGesture {
if self.text == "Burger" {
self.text = "CheeseBurger"
} else {
self.text = "Burger"
}
}
}
}
}
And there is no need to reference the imageView and tapGesture at all.
Related
I am trying to implement a simple 3-screen application on SwiftUI. The 1st screen allows the user to select a photo, the 2nd must show the loading screen while the photo is analyzed, and the 3rd shows analysis results. Currently, my application doesn't show the loading screen at all and just jumps to the 3rd screen.
ContentView.swift:
struct ContentView: View {
#State var image: Image = Image("MyImageName")
#State var tiles: [Tile] = []
#State var isSelectionView: Bool = true
#State var isLoadingView: Bool = false
#State var isAdjustmentView: Bool = false
var body: some View {
if (isSelectionView) {
SelectionView(image: $image, isSelectionView: $isSelectionView, isLoadingView: $isLoadingView)
}
if (isLoadingView) {
LoadingMLView(image: $image, tiles: $tiles, isLoadingView: $isLoadingView, isAdjustmentView: $isAdjustmentView)
}
if (isAdjustmentView) {
AdjustmentView(tiles: $tiles)
}
}}
SelectionView.swift:
struct SelectionView: View {
#State var showCaptureImageView = false
#State var showPopover = false
#State var sourceType: UIImagePickerController.SourceType = .camera
#State var imageSelected = false
#Binding var image: Image
#Binding var isSelectionView: Bool
#Binding var isLoadingView: Bool
var body: some View {
VStack() {
if (!showCaptureImageView && !imageSelected) {
Spacer()
Button(action: {
showPopover.toggle()
}) {
Text("Choose photo")
.frame(width: 100, height: 100)
.foregroundColor(Color.white)
.background(Color.blue)
.clipShape(Circle())
}
.confirmationDialog("Select location", isPresented: $showPopover) {
Button("Camera") {
sourceType = .camera
showPopover.toggle()
showCaptureImageView.toggle()
}
Button("Photos") {
sourceType = .photoLibrary
showPopover.toggle()
showCaptureImageView.toggle()
}
}
} else if (showCaptureImageView) {
CaptureImageView(isShown: $showCaptureImageView, image: $image, sourceType: $sourceType)
} else {
ActivityIndicator()
.frame(width: 200, height: 200)
.foregroundColor(.blue)
}
}
.onChange(of: image) { image in
showCaptureImageView.toggle()
imageSelected.toggle()
isSelectionView.toggle()
isLoadingView.toggle()
}
}}
LoadingMLView.swift:
struct LoadingMLView: View {
#Binding var image: Image
#Binding var tiles: [Tile]
#Binding var isLoadingView: Bool
#Binding var isAdjustmentView: Bool
var body: some View {
ActivityIndicator()
.frame(width: 200, height: 200)
.foregroundColor(.blue)
.onAppear {
do {
let model = try VNCoreMLModel(for: MyModel().model)
let request = VNCoreMLRequest(model: model, completionHandler: myResultsMethod)
let uiImage: UIImage = image.asUIImage()
guard let ciImage = CIImage(image: uiImage) else {
fatalError("Unable to create \(CIImage.self) from \(image).")
}
let handler = VNImageRequestHandler(ciImage: ciImage)
do {
try handler.perform([request])
} catch {
print("Something went wrong!")
}
func myResultsMethod(request: VNRequest, error: Error?) {
guard let results = request.results as? [VNRecognizedObjectObservation]
else { fatalError("error") }
for result in results {
let tile = Tile(name: result.labels[0].identifier)
tiles.append(tile)
}
isLoadingView.toggle()
isAdjustmentView.toggle()
}
} catch {
print("Error:", error)
}
}
}}
No matter how I change the toggle between screens, it just doesn't seem reactive and the views don't switch momentarily. What am I doing wrong? Is there a proper way to show loading screen right after the image is selected?
Seems like you're missing an else to avoid rendering the screens you don't want:
var body: some View {
if (isSelectionView) {
SelectionView(image: $image, isSelectionView: $isSelectionView, isLoadingView: $isLoadingView)
} else if (isLoadingView) {
LoadingMLView(image: $image, tiles: $tiles, isLoadingView: $isLoadingView, isAdjustmentView: $isAdjustmentView)
} else if (isAdjustmentView) {
AdjustmentView(tiles: $tiles)
}
}}
But, in general what you want is also called "state machine" - essentially your app can be in one of 3 possible states: selection, loading, adjustment, so you should have an enum #State to represent the current state, instead of the 3 bool values:
struct ContentView: View {
// ...
enum AppState { case selection, loading, adjustment }
#State private var state = AppState.selection
var body: some View {
switch state {
case .selection:
SelectionView(image: $image, state: $state)
case .loading:
LoadingMLView(image: $image, tiles: $tiles, state: $state)
case .adjustment:
AdjustmentView(tiles: $tiles)
}
}
When you want to switch to the next screen, just change state value...
Turns out, the main performance issue was caused by the image type conversion:
let uiImage: UIImage = image.asUIImage()
After passing the image as UIImage in the first place, the app started working fast, so there was no need to handle the loading in the first place.
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?
I have a NavigationLink within a LazyVGrid and am getting this animation on return from the details view. Starting at about 3.5 seconds into that video, there is an animation I wasn't expecting. There are gaps introduced between the cells and I don't like the way it looks.
Here is the code for the screen with the LazyVGrid:
import Foundation
import SwiftUI
import SFSafeSymbols
import CustomModalView
struct AlbumItemsScreen: View {
#ObservedObject var viewModel:AlbumItemsViewModel
let gridItemLayout = [GridItem(.adaptive(minimum: 50), spacing: 20)]
#State var object: ServerObjectModel?
#State var enableNavLink: Bool = false
var body: some View {
RefreshableScrollView(refreshing: $viewModel.loading) {
LazyVGrid(columns: gridItemLayout) {
ForEach(viewModel.objects, id: \.fileGroupUUID) { item in
AlbumItemsScreenCell(object: item)
.onTapGesture {
object = item
viewModel.showCellDetails = true
}
// Without this conditional, "spacer" cells show up in the grid.
if viewModel.showCellDetails, let object = object {
// The `NavigationLink` works here because the `MenuNavBar` contains a `NavigationView`.
NavigationLink(
destination:
// If I just use `item` directly in this-- oddly, it doesn't reference the same object as for `AlbumItemsScreenCell` above.
ObjectDetailsView(object: object),
isActive:
$viewModel.showCellDetails) {
}
} // end if
} // end ForEach
} // end LazyVGrid
.padding(10)
}
.alert(isPresented: $viewModel.presentAlert, content: {
let message:String = viewModel.alertMessage
viewModel.alertMessage = nil
return Alert(title: Text(message))
})
.navigationBarTitle("Album Contents")
.navigationBarItems(trailing:
AlbumItemsScreenNavButtons(viewModel: viewModel)
)
.disabled(viewModel.addNewItem)
.modal(isPresented: $viewModel.addNewItem) {
AddItemModal(viewModel: viewModel)
.padding(20)
}
.modalStyle(DefaultModalStyle())
.onDisappear {
// I'm having a problem with the modal possibly being presented, the user navigating away, coming back and the modal still being present.
// See also https://github.com/jankaltoun/CustomModalView/issues/1
if viewModel.addNewItem == true {
viewModel.addNewItem = false
}
}
}
}
private struct AlbumItemsScreenNavButtons: View {
#ObservedObject var viewModel:AlbumItemsViewModel
var body: some View {
HStack(spacing: 0) {
Button(
action: {
viewModel.sync()
},
label: {
SFSymbolNavBar(symbol: .goforward)
}
)
Button(
action: {
viewModel.startNewAddItem()
},
label: {
SFSymbolNavBar(symbol: .plusCircle)
}
)
}
}
}
(see also https://github.com/SyncServerII/Neebla/blob/main/Neebla/UI/Screens/Album%20Items/AlbumItemsScreen.swift).
Here is the code for the details view:
import Foundation
import SwiftUI
import SFSafeSymbols
struct ObjectDetailsView: View {
let object:ServerObjectModel
var model:MessagesViewModel?
#State var showComments = false
init(object:ServerObjectModel) {
self.object = object
model = MessagesViewModel(object: object)
}
var body: some View {
VStack {
AnyLargeMedia(object: object)
.onTapGesture {
if let _ = model {
showComments = true
}
}
Spacer()
}
.navigationBarItems(trailing:
Button(
action: {
showComments = true
},
label: {
SFSymbolNavBar(symbol: .message)
}
)
.enabled(model != nil)
)
.sheet(isPresented: $showComments) {
if let model = model {
CommentsView(model: model)
}
else {
// Should never get here. Should never have showComments == true when model is nil.
EmptyView()
}
}
}
}
(see also https://github.com/SyncServerII/Neebla/blob/main/Neebla/UI/Screens/Object%20Details/ObjectDetailsView.swift).
I've tried the strategy indicated here https://developer.apple.com/forums/thread/125937 with this:
NavigationLink(
destination:
// If I just use `item` directly in this-- oddly, it doesn't reference the same object as for `AlbumItemsScreenCell` above.
ObjectDetailsView(object: object),
isActive:
$viewModel.showCellDetails) {
EmptyView()
}
.frame(width: 0, height: 0)
.disabled(true)
but the same effect occurs.
Well, it helped to focus my attention on the problem by writing up this question. I've come up with a solution. I took the NavigationLink out of the scrollview and LazyVGrid:
import Foundation
import SwiftUI
import SFSafeSymbols
import CustomModalView
struct AlbumItemsScreen: View {
#ObservedObject var viewModel:AlbumItemsViewModel
let gridItemLayout = [GridItem(.adaptive(minimum: 50), spacing: 20)]
#State var object: ServerObjectModel?
var body: some View {
VStack {
RefreshableScrollView(refreshing: $viewModel.loading) {
LazyVGrid(columns: gridItemLayout) {
ForEach(viewModel.objects, id: \.fileGroupUUID) { item in
AlbumItemsScreenCell(object: item)
.onTapGesture {
object = item
viewModel.showCellDetails = true
}
} // end ForEach
} // end LazyVGrid
.padding(10)
}
if let object = object {
// The `NavigationLink` works here because the `MenuNavBar` contains a `NavigationView`.
NavigationLink(
destination:
ObjectDetailsView(object: object),
isActive:
$viewModel.showCellDetails) {
EmptyView()
}
.frame(width: 0, height: 0)
.disabled(true)
} // end if
}
.alert(isPresented: $viewModel.presentAlert, content: {
let message:String = viewModel.alertMessage
viewModel.alertMessage = nil
return Alert(title: Text(message))
})
.navigationBarTitle("Album Contents")
.navigationBarItems(trailing:
AlbumItemsScreenNavButtons(viewModel: viewModel)
)
.disabled(viewModel.addNewItem)
.modal(isPresented: $viewModel.addNewItem) {
AddItemModal(viewModel: viewModel)
.padding(20)
}
.modalStyle(DefaultModalStyle())
.onDisappear {
// I'm having a problem with the modal possibly being presented, the user navigating away, coming back and the modal still being present.
// See also https://github.com/jankaltoun/CustomModalView/issues/1
if viewModel.addNewItem == true {
viewModel.addNewItem = false
}
}
}
}
private struct AlbumItemsScreenNavButtons: View {
#ObservedObject var viewModel:AlbumItemsViewModel
var body: some View {
HStack(spacing: 0) {
Button(
action: {
viewModel.sync()
},
label: {
SFSymbolNavBar(symbol: .goforward)
}
)
Button(
action: {
viewModel.startNewAddItem()
},
label: {
SFSymbolNavBar(symbol: .plusCircle)
}
)
}
}
}
I'm working on my project with the feature of select multiple blocks of thumbnails. Only selected thumbnail(s)/image will be highlighted.
For the ChildView, The binding activeBlock should be turned true/false if a use taps on the image.
However, when I select a thumbnail, all thumbnails will be highlighted.I have come up with some ideas like
#State var selectedBlocks:[Bool]
// which should contain wether or not a certain block is selected.
But I don't know how to implement it.
Here are my codes:
ChildView
#Binding var activeBlock:Bool
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color("orange"))
}
}
}
BlockBView
struct VideoData: Identifiable{
var id = UUID()
var thumbnails: String
}
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "test"), VideoData(thumbnails: "test2"), VideoData(thumbnails: "test1")
]
#State var activeBlock = false
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(0..<videos.count) { _ in
Button(action: {
self.activeBlock.toggle()
}, label: {
ChildView(activeBlock: $activeBlock, thumbnail: "test")
})
}
}
}
}
Thank you for your help!
Here is a demo of possible approach - we initialize array of Bool by videos count and pass activated flag by index into child view.
Tested with Xcode 12.1 / iOS 14.1 (with some replicated code)
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "flag-1"), VideoData(thumbnails: "flag-2"), VideoData(thumbnails: "flag-3")
]
#State private var activeBlocks: [Bool] // << declare
init() {
// initialize state with needed count of bools
self._activeBlocks = State(initialValue: Array(repeating: false, count: videos.count))
}
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(videos.indices, id: \.self) { i in
Button(action: {
self.activeBlocks[i].toggle() // << here !!
}, label: {
ChildView(activeBlock: activeBlocks[i], // << here !!
thumbnail: videos[i].thumbnails)
})
}
}
}
}
}
struct ChildView: View {
var activeBlock:Bool // << value, no binding needed
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color.orange)
}
}
}
}
}
Final result
Build your element and it's model first. I'm using MVVM,
class RowModel : ObservableObject, Identifiable {
#Published var isSelected = false
#Published var thumnailIcon: String
#Published var name: String
var id : String
var cancellables = Set<AnyCancellable>()
init(id: String, name: String, icon: String) {
self.id = id
self.name = name
self.thumnailIcon = icon
}
}
//Equivalent to your BlockView
struct Row : View {
#ObservedObject var model: RowModel
var body: some View {
GroupBox(label:
Label(model.name, systemImage: model.thumnailIcon)
.foregroundColor(model.isSelected ? Color.orange : .gray)
) {
HStack {
Capsule()
.fill(model.isSelected ? Color.orange : .gray)
.onTapGesture {
model.isSelected = !model.isSelected
}
//Two way binding
Toggle("", isOn: $model.isSelected)
}
}.animation(.spring())
}
}
Prepare data and handle action in your parent view
struct ContentView: View {
private let layout = [GridItem(.flexible()),GridItem(.flexible())]
#ObservedObject var model = ContentModel()
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: layout) {
ForEach(model.rowModels) { model in
Row(model: model)
}
}
}
if model.selected.count > 0 {
HStack {
Text(model.selected.joined(separator: ", "))
Spacer()
Button(action: {
model.clearSelection()
}, label: {
Text("Clear")
})
}
}
}
.padding()
.onAppear(perform: prepare)
}
func prepare() {
model.prepare()
}
}
class ContentModel: ObservableObject {
#Published var rowModels = [RowModel]()
//I'm handling by ID for futher use
//But you can convert to your Array of Boolean
#Published var selected = Set<String>()
func prepare() {
for i in 0..<20 {
let row = RowModel(id: "\(i)", name: "Block \(i)", icon: "heart.fill")
row.$isSelected
.removeDuplicates()
.receive(on: RunLoop.main)
.sink(receiveValue: { [weak self] selected in
guard let `self` = self else { return }
print(selected)
if selected {
self.selected.insert(row.name)
}else{
self.selected.remove(row.name)
}
}).store(in: &row.cancellables)
rowModels.append(row)
}
}
func clearSelection() {
for r in rowModels {
r.isSelected = false
}
}
}
Don't forget to import Combine framework.
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()
}
}