SwiftUI View re-renders before model updates - ios

Why is my view rendering the model update, before the update has completed? Or at least it seems that way. It seems like, it re-draws based on the previous value of my model.
What I'm doing: When you tap a person cell, it marks that person as "selected" and then changes the foreground color to green. If they are not selected, the foreground color is white.
What is happening: It is important to note that this doesn't ALWAYS happen. It seems random. But occasionally, when I tap on a person cell, the color does not change. And I've dug deeper, and that is because it (looks like) it redraws the view before the value has changed inside my PeopleStore model object.
Here is the code:
struct PersonCell: View {
#EnvironmentObject var peopleStore: PeopleStore
var personId: UUID
var person: Person {
return peopleStore.people.filter({ $0.id == personId }).first!
}
var body: some View {
Button(action: {
print("Tapped Cell: " + peopleStore.personName(id: personId))
peopleStore.toggleSelectionOfPerson(with: personId)
}) {
GeometryReader { geo in
ZStack {
Rectangle().foregroundColor(peopleStore.personIsSelected(id: personId) ? .tGreen : .white).cornerRadius(geo.size.width / 2).frame(height: 70)
Here is what happens when I tap a cell:
class PeopleStore: ObservableObject {
#Published var people : [Person]
init(people: [Person]){
self.people = people
}
func personIsSelected(id: UUID) -> Bool {
let index = people.firstIndex(where: { $0.id == id })!
print("ZSTACK: \(people[index].name) is selected: \(people[index].isSelected.description)")
return people[index].isSelected
}
func toggleSelectionOfPerson(with id: UUID) {
let index = people.firstIndex(where: { $0.id == id })!
print("BEFORE \(people[index].name) is selected: \(people[index].isSelected.description)")
people[index].isSelected = !people[index].isSelected
print("AFTER \(people[index].name) is selected: \(people[index].isSelected.description)")
}
And when I look at my console, you can see that after I select a person, the view redraws, but it has the previous value (In this case, I tapped the Alexandra cell):
Tapped Cell: Alexandra R.
BEFORE Alexandra R. is selected: false
ZSTACK: Jose G. is selected: false
ZSTACK: Jimmy T. is selected: false
ZSTACK: Alexandra R. is selected: false
ZSTACK: Dominic R. is selected: false
AFTER Alexandra R. is selected: true
ZSTACK: Franny E. is selected: false
ZSTACK: Jon B. is selected: true
ZStack: is printed whenever the cell is re-drawn. As you can see, Alexandra is selected: false shows up, when I would expect it to be true.
The timing of the re-draw seems to be just a bit off, and it seems random. What am I missing?
EDIT: Person struct
struct Person: Identifiable {
let id = UUID()
var name: String
var photo: Image
var isFavorite: Bool
var dateFavorited = Date()
var isSelected: Bool = false
}
Edit 2: If it helps, the PersonCell is a subview of something called a PersonList that uses ForEach to generate PersonCells.
struct PeopleList: View {
#EnvironmentObject var peopleStore: PeopleStore
var title: String {
return isFavorites ? "Favorites" : "All Employees"
}
var isFavorites: Bool
var body: some View {
LazyVStack(alignment: .leading, spacing: 15) {
Text(title).padding(.top, 18).padding(.horizontal, 5).font(.regular())
if isFavorites {
ForEach(peopleStore.people.sorted(by: { $0.dateFavorited < $1.dateFavorited }).filter({ $0.isFavorite })) { person in
PersonCell(personId: person.id).frame(height: 70.0)
}

I am not sure what exactly is going wrong at your end, i have tested your code at my end and it’s working perfectly fine. The only change I have done is set var isFavorites: Bool = true in PeopleList because i don’t know how you are toggling that property in your code.So, I have provided it a constant value.
Below is the code -:
struct Person: Identifiable {
let id = UUID()
var name: String
var photo: Image
var isFavorite: Bool
var dateFavorited = Date()
var isSelected: Bool = false
}
class PeopleStore: ObservableObject {
#Published var people : [Person]
init(people: [Person]){
self.people = people
}
func personIsSelected(id: UUID) -> Bool {
let index = people.firstIndex(where: { $0.id == id })!
print("ZSTACK: \(people[index].name) is selected: \(people[index].isSelected.description)")
return people[index].isSelected
}
func personName(id: UUID) -> String {
let index = people.firstIndex(where: { $0.id == id })!
return people[index].name
}
func toggleSelectionOfPerson(with id: UUID) {
let index = people.firstIndex(where: { $0.id == id })!
print("BEFORE \(people[index].name) is selected: \(people[index].isSelected.description)")
people[index].isSelected = !people[index].isSelected
print("AFTER \(people[index].name) is selected: \(people[index].isSelected.description)")
}
}
struct PeopleList: View {
#EnvironmentObject var peopleStore:PeopleStore
var title: String {
return isFavorites ? "Favorites" : "All Employees"
}
var isFavorites: Bool = true
var filteredPersons:[Person]{
peopleStore.people.sorted(by: { $0.dateFavorited < $1.dateFavorited }).filter({ $0.isFavorite })
}
var body: some View {
LazyVStack(alignment: .leading, spacing: 15) {
Text(title).padding(.top, 18)
if isFavorites {
ForEach(filteredPersons) { person in
PersonCell(personId: person.id).frame(height: 70.0)
}
}
}
}
}
struct PersonCell: View {
#EnvironmentObject var peopleStore:PeopleStore
var personId: UUID
var body: some View {
Button(action: {
print("Tapped Cell: " + peopleStore.personName(id: personId))
peopleStore.toggleSelectionOfPerson(with: personId)
}) {
GeometryReader { geo in
ZStack {
Rectangle().foregroundColor(peopleStore.personIsSelected(id: personId) ? .green : .gray).cornerRadius(geo.size.width / 2).frame(height: 70)
}
}
}
}
}
Main View-:
import SwiftUI
#main
struct Test: App {
#StateObject var store:PeopleStore
init() {
let person = Person(name: "foo", photo: Image(""), isFavorite: true)
let person1 = Person(name: "foo1", photo: Image(""), isFavorite: true)
let person2 = Person(name: "foo2", photo: Image(""), isFavorite: true)
let obj = PeopleStore(people: [person,person1,person2])
_store = StateObject(wrappedValue: obj)
}
var body: some Scene {
WindowGroup {
PeopleList().environmentObject(store)
}
}
}
Output-:
Tapped Cell: foo
BEFORE foo is selected: false
AFTER foo is selected: true
ZSTACK: foo2 is selected: false
ZSTACK: foo is selected: true
ZSTACK: foo1 is selected: false
Note-: I have tested multiple times, and didn’t found any issue where selected cell isn’t changing color.

Related

SwiftUI Stepper not working when bound to a model property directly

I am fairly new to iOS development. I am trying to update the property "cars" in the "Sire" model using a stepper. Whenever I press on + or - from the stepper controls, it changes the value by a step and then becomes disabled.
If I bind the stepper to the variable cars, it works flawlessly.
struct AddSireView: View {
// #EnvironmentObject var sireVM:SiresViewModel
#State var newSire = Sire (id:"", name: "", ownerID: 0, info:"", achievments: "", cars: 0, cups: 0)
#State var cars = 0
#State var cups = 0
#State private var state = FormState.idle
var createAction: CreateAction
// TODO: Put validation that the added sire is valid and if not show errors to the user
var body: some View {
Form {
VStack (spacing: 18) {
TitledTextView(text: $newSire.name, placeHolder: "الاسم", title: "الاسم")
TiltedTextEditor(text: Binding<String>($newSire.info)!, title: "معلومات البعير")
TiltedTextEditor(text: Binding<String>($newSire.achievments)!, title: "انجازات البعير")
}
Stepper(value: $newSire.cars, in: 0...10,step:1) {
HStack {
Text ("سيارات:")
TextField("Cars", value: $newSire.cars, formatter: NumberFormatter.decimal)
}
}
And this is the "Sire" struct
struct Sire: Hashable, Identifiable, Decodable {
static func == (lhs: Sire, rhs: Sire) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name && lhs.ownerID == rhs.ownerID
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(name)
hasher.combine(ownerID)
}
var id:String
var name:String
var ownerID:Int
var fatherID:String?
var info:String?
var achievments:String?
var cars:Int = 0
var cups:Int = 0
init (id:String, name:String, ownerID:Int, info:String? = nil, achievments:String? = nil,
fatherID:String? = nil, cars:Int = 0, cups:Int = 0) {
self.id = id
self.name = name
self.ownerID = ownerID
self.cars = cars
self.cups = cups
self.info = info
self.achievments = achievments
}
}
"Sire" was a class and i made it a Struct thinking that that was the problem, but to no avail.
Consider this approach using an ObservableObject to hold your Sire. This allows you to use
both the Stepper and the Textfield at the same time.
struct ContentView: View {
#StateObject var sireModel = SireModel() // <-- here
var body: some View {
Form {
Stepper(value: $sireModel.sire.cars, in: 0...10, step:1) {
HStack {
Text ("سيارات: ")
TextField("", value: $sireModel.sire.cars, formatter: NumberFormatter())
}
}
}
}
}
class SireModel: ObservableObject {
#Published var sire: Sire = Sire(id:"", name: "", ownerID: 0, info:"", achievments: "", cars: 0, cups: 0)
}
Get rid of the custom implementations for Equatable and Hashable (func == and func hash) you don't include cars in it so SwiftUI doesn't know when to reload.
SwiftUI is all about identity if you change how Swift computes the identity (using Hashable, Equatable and Identifiable) you change the behavior.
Check out Demystify SwiftUI
The video above is the "best" place to learn about the concept.

Toggle Required Multiple Taps to Work in SwiftUI

I have a List in SwiftUI, which displays books. Each book has a isFinished property. When the user taps on the book, it takes them to the detail page where they can toggle the isFinished status. The problem, I am facing is that on the BookDetailView, when the user taps on the Toggle it does not change the toggle status on the first tap. I can tap twice to change the Toggle from off to on etc.
Here is my complete code:
struct Book: Hashable, Identifiable {
let id = UUID()
let name: String
var isFinished: Bool = true
}
struct BookCellView: View {
let book: Book
var body: some View {
HStack {
Text(book.name)
Text(book.isFinished ? "Finished": "Not finished")
}
}
}
struct BookDetailView: View {
#Binding var book: Book
#State private var isOn: Bool = false
var body: some View {
VStack {
Text(book.name)
Toggle(isOn: $book.isFinished) {
Text("Finished")
}.fixedSize()
}
}
}
struct ContentView: View {
#State private var books = [Book(name: "Harry Potter"), Book(name: "Peek Performance"), Book(name: "Ready Player One")]
func bookBinding(id: UUID) -> Binding<Book> {
let index = books.firstIndex(where: { $0.id == id })!
return Binding {
return books[index]
} set: {
books[index] = $0
}
}
var body: some View {
NavigationStack {
List(books) { book in
NavigationLink(value: book) {
BookCellView(book: book)
}
}.navigationDestination(for: Book.self) { book in
BookDetailView(book: bookBinding(id: book.id))
}
}
}
}
Any ideas what I am missing?
UPDATE 1:
struct ContentView: View {
#State private var books = [Book(name: "Harry Potter"), Book(name: "Peek Performance"), Book(name: "Ready Player One")]
/*
func bookBinding(id: UUID) -> Binding<Book> {
let index = books.firstIndex(where: { $0.id == id })!
return Binding {
return books[index]
} set: {
books[index] = $0
}
} */
var body: some View {
NavigationStack {
List(books) { book in
NavigationLink(value: book.id) {
BookCellView(book: book)
}
}.navigationDestination(for: UUID.self) { id in
let index = books.firstIndex(where: { $0.id == id })!
BookDetailView(book: $books[index])
}
}
}
}

'SwiftUI' how to change checkmark in `listview`

I'm trying to change the checkmark image on listview row selection, but I'm not getting expected result from selection.
Below code I'm creating listview from the array todo items and added tapGesture to the row. When I'm selecting row I can able to change the completed value but not reflecting that on UI.
struct ContentView: View {
#State var items: [Todo] = todoItems
var body: some View {
List(items, id: \.self) { item in
Row(item: item) { item in
item.completed.toggle()
print(item.completed)
}
}
}
}
struct Row: View {
let item: Todo
var onTap: ((_ item: Todo) -> Void)?
var body: some View {
HStack {
Image(systemName: item.completed ? "checkmark.square.fill" : "squareshape" )
.foregroundColor(item.completed ? .green : .gray)
Text(item.title).font(.headline).foregroundColor(.secondary)
}.onTapGesture {
onTap?(item)
}
}
}
class Todo: Hashable {
static func == (lhs: Todo, rhs: Todo) -> Bool {
lhs.title == rhs.title
}
var title: String
var completed: Bool
init(title: String, completed: Bool) {
self.title = title
self.completed = completed
}
func hash(into hasher: inout Hasher) {
hasher.combine(title)
}
}
let todoItems = [
Todo(title: "$300", completed: false),
Todo(title: "$550", completed: false),
Todo(title: "$450", completed: false)
]
You can make Todo a struct. For data like this, it is much more preferable for it to be a struct rather than a class.
The item also doesn't need to be passed down, since you already get it in the List. I also used the $ syntax within for $items, so $item is a Binding and you therefore can mutate item. Changing item is changing that element in #State variable items, therefore a view update occurs.
Your implementation for Equatable and Hashable on Todo was incorrect - because otherwise the view won't update if a Todo instance is the same, even with a different completed value. I added Identifiable to Todo anyway, so you can implicitly identify by id.
Code:
struct ContentView: View {
#State var items: [Todo] = todoItems
var body: some View {
List($items) { $item in
Row(item: item) {
item.completed.toggle()
}
}
}
}
struct Row: View {
let item: Todo
let onTap: (() -> Void)?
var body: some View {
HStack {
Image(systemName: item.completed ? "checkmark.square.fill" : "squareshape" )
.foregroundColor(item.completed ? .green : .gray)
Text(item.title).font(.headline).foregroundColor(.secondary)
}.onTapGesture {
onTap?()
}
}
}
struct Todo: Hashable, Identifiable {
let id = UUID()
var title: String
var completed: Bool
init(title: String, completed: Bool) {
self.title = title
self.completed = completed
}
}

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)
}
}
}
}

List Item with Picker (segmented control) subview and selection gesture

I have a List and items containing a Picker view (segmented control stye). I would like to handle selection and picker state separately. the picker should be disabled when list item is not selected.
The problem is:
first tap - selects the list item. (selected = true)
tap on picker - set selection = false
On UIKit, Button/SegmentControl/etc... catch the 'tap' and do not pass through to TableView selection state.
struct ListView: View {
#State var selected = Set<Int>()
let items = (1...10).map { ItemDataModel(id: $0) }
var body: some View {
List(items) { item in
ListItemView(dataModel: item)
.onTapGesture { if !(selected.remove(item.id) != .none) { selected.insert(item.id) }}
}
}
}
struct ListItemView: View {
#ObservedObject var dataModel: ItemDataModel
var body: some View {
let pickerBinding = Binding<Int>(
get: { dataModel.state?.rawValue ?? -1 },
set: { dataModel.state = DataState(rawValue: $0) }
)
HStack {
Text(dataModel.title)
Spacer()
Picker("sdf", selection: pickerBinding) {
Text("State 1").tag(0)
Text("State 2").tag(1)
Text("State 3").tag(2)
}.pickerStyle(SegmentedPickerStyle())
}
}
}
enum DataState: Int {
case state1, state2, state3
}
class ItemDataModel: Hashable, Identifiable, ObservableObject {
let id: Int
let title: String
#Published var state: DataState? = .state1
init(id: Int) {
self.id = id
title = "item \(id)"
}
func hash(into hasher: inout Hasher) {
hasher.combine(title)
}
static func == (lhs: ItemDataModel, rhs: ItemDataModel) -> Bool {
return lhs.id == rhs.id
}
}
[Please try to ignore syntax errors (if exsist) and focus on the gesture issue]
A possible solution is to apply modifiers only when the Picker is not selected:
struct ListView: View {
#State var selected = Set<Int>()
let items = (1 ... 10).map { ItemDataModel(id: $0) }
var body: some View {
List(items) { item in
if selected.contains(item.id) {
ListItemView(dataModel: item)
} else {
ListItemView(dataModel: item)
.disabled(true)
.onTapGesture {
if !(selected.remove(item.id) != .none) {
selected.insert(item.id)
}
}
}
}
}
}

Resources