Compare Button Label value to string variable - string-comparison

I'm trying to write a simple quiz app. Upon a user tapping a button, I want the action to compare the Button's label to see if it matches the variable of "correctChoice"
I tried the following if/then statement but it doesn't seem to work, as I'm getting the following error "Left side of mutating operator isn't mutable: 'self' is immutable"
//Variables Used
var model = Question.all()
var questionNumber: Int = 1
var totalQuestions: Int = 100
var timeLeft: Int = 60
var currentScore: Int = 0
//Button Code
Button(action: {
if Question.all()[0].choice1 == Question.all()[0].correctChoice {
currentScore += 10
} else {
currentScore += 0
}
}) {
Text(Question.all()[0].choice1)
.font(.system(size: 20))
}

currentScore should be a #State variable to be mutable inside a struct. Or it can be inside a model.

Related

Change of selected row in Table for macOS

I use Table in my CoreData app for macOS
#FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)]) private var categories: FetchedResults<Categories>
#State private var selectedCategoryID: Categories.ID?
and I'm wondering how I could catch change of selected row. If I try .onChange() as shown below then code can't be compiled due to error "Cannot call value of non-function type 'Categories.ID?' (aka 'Optional<Optional>')"
Table(categories, selection: $selectedCategoryID) {
TableColumn("Name", value \.name!)
TableColumn("Type", value: \.type!)
}.onChange(of: selectedCategoryID {
print("Selected row changed.")
}
Thank you.
onChange is "to trigger a side effect", e.g. something unrelated. If you want to update a related value, then a trick is to move it into a struct with some logic, e.g.
struct TableConfig {
var selectedCategoryID: CategoryID? {
didSet {
counter++
}
}
var counter = 0
mutating func reset() {
selectedCategoryID = nil
counter = 0
}
}
...
#State var config = TableConfig()
...
Table(categories, selection: $config.selectedCategoryID)
I finally found the root cause .onChange() must looks like
.onChange(of: selectedCategoryID) { selected in
print("Selected row is \(selected)")
}

In SwiftUI how do I animate changes one at a time when they occur in a called method?

Although I get an animation when I tap the button, it's not the animation I want.
The entire view is being replaced at once, but I want to see each element change in sequence. I tried in both the parent view and in the called method. Neither produces the desired result.
(this is a simplified version of the original code)
import SwiftUI
struct SequencedCell: Identifiable {
let id = UUID()
var value: Int
mutating func addOne() {
value += 1
}
}
struct AQTwo: View {
#State var cells: [SequencedCell]
init() {
_cells = State(initialValue: (0 ..< 12).map { SequencedCell(value: $0) })
}
var body: some View {
VStack {
Spacer()
Button("+") {
sequencingMethod(items: $cells)
}
.font(.largeTitle)
Spacer()
HStack {
ForEach(Array(cells.enumerated()), id: \.1.id) { index, item in
// withAnimation(.linear(duration: 4)) {
Text("\(item.value)").tag(index)
// }
}
}
Spacer()
}
}
func sequencingMethod(items: Binding<[SequencedCell]>) {
for cell in items {
withAnimation(.linear(duration: 4)) {
cell.wrappedValue = SequencedCell(value: cell.wrappedValue.value + 1)
// cell.wrappedValue.addOne()
}
}
}
}
struct AQTwoPreview: PreviewProvider {
static var previews: some View {
AQTwo()
}
}
So I want the 0 to turn into a 1, the 1 then turn into a 2, etc.
Edit:
Even though I have accepted an answer, it answered my question, but didn't solve my issue.
I can't use DispatchQueue.main.asyncAfter because the value I am updating is an inout parameter and it makes the compiler unhappy:
Escaping closure captures 'inout' parameter 'grid'
So I tried Malcolm's (malhal) suggestion to use delay, but everything happens immediately with no sequential animation (the entire block of updated items animate as one)
Here's the recursive method I am calling:
static func recursiveAlgorithm(targetFill fillValue: Int, in grid: inout [[CellItem]],
at point: (x: Int, y: Int), originalFill: Int? = nil, delay: TimeInterval) -> [[CellItem]] {
/// make sure the point is on the board (or return)
guard isValidPlacement(point) else { return grid }
/// the first time this is called we don't have `originalFill`
/// so we read it from the starting point
let tick = delay + 0.2
//AnimationTimer.shared.tick()
let startValue = originalFill ?? grid[point.x][point.y].value
if grid[point.x][point.y].value == startValue {
withAnimation(.linear(duration: 0.1).delay(tick)) {
grid[point.x][point.y].value = fillValue
}
_ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x, point.y - 1), originalFill: startValue, delay: tick)
_ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x, point.y + 1), originalFill: startValue, delay: tick)
_ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x - 1, point.y), originalFill: startValue, delay: tick)
_ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x + 1, point.y), originalFill: startValue, delay: tick)
}
return grid
}
Further comments/suggestions are welcome, as I continue to wrestle with this.
As mentioned in the comments, the lowest-tech version is probably just using a DisatpchQueue.main.asyncAfter call:
func sequencingMethod(items: Binding<[SequencedCell]>) {
var wait: TimeInterval = 0.0
for cell in items {
DispatchQueue.main.asyncAfter(deadline: .now() + wait) {
withAnimation(.linear(duration: 1)) {
cell.wrappedValue = SequencedCell(value: cell.wrappedValue.value + 1)
}
}
wait += 1.0
}
}
You could use delay(_:) for that, e.g.
func sequencingMethod(items: Binding<[SequencedCell]>) {
var delayDuration = 0.0
for cell in items {
withAnimation(.linear(duration: 4).delay(delayDuration)) {
cell.wrappedValue = SequencedCell(value: cell.wrappedValue.value + 1)
}
delayDuration += 0.5
}
}

How to change the sort comparator or display Int? values on a Table with SwiftUI on macOS

I have the following table that mostly works when all the column values are strings:
struct Person : Identifiable {
let id: String // assume names are unique for this example
let name: String
let rank: String
}
#State var people = []
#State var sort = [KeyPathComparitor(\Person.rank)]
#State var selection: Person.ID? = nil
private var rankingsCancellable: AnyCancellable? = nil
init (event: Event) {
rankingsCancellable = event.rankingsSubject
.receive(on: DispatchQueue.main)
.sink { rankings in
var newRankings: [Person] = []
for ranking in rankings {
newRankings.append(Person(id: ranking.name, name: ranking.name, ranking: ranking.rank
}
newRankings.sort(by: sort)
people = newRankings
}
}
var body : some View {
Table(people, selection: $selection, sortOrder: $sort) {
TableColumn("Name", value: \Person.name)
TableColumn("Rank", value: \Person.rank)
}
.onChange(of: sortOrder) {
people.sort(using: $0)
}
}
This works well enough with one exception when the rank is unknown coming into the sink, its sent as an empty string and the empty strings sort before those with valid ranking. Ideally, I'd like to have those without a ranking sort after until their rank is received.
I've tried a couple of things, changing Person.rank to an Int but the compiler gives the typical 'too complex' error when:
TableColumn("Rank", value: \Person.rank) { Text(\($0.rank)) } // rank is an int here
Alternatively I've tried to create a custom sort comparator when creating the KeyPathComparator(\Person.rank) but the documentation around that is limited and haven't been able to suss out a workable solution.
Any ideas how to get the Table to simply display Ints (which I imagine would have to be optionals in this instance) or add a string comparator that moves empty strings below populated strings?
TIA
This is the best answer I could find, tho admit it does seem a bit like a kludge. Basically adding Int property for sorting and setting the sort to initially sort on this Person property
struct Person : Identifiable {
let id: String // assume names are unique for this example
let name: String
let rank: String
//Add rankForSort as an int which is a non displayed column
let rankForSort: Int
}
#State var people = []
//Change to initial sort to non-displayed rank field
#State var sort = [KeyPathComparitor(\Person.rankForSort)]
#State var selection: Person.ID? = nil
private var rankingsCancellable: AnyCancellable? = nil
init (event: Event) {
rankingsCancellable = event.rankingsSubject
.receive(on: DispatchQueue.main)
.sink { rankings in
var newRankings: [Person] = []
for ranking in rankings {
// Ranking now coming through in the subject as Int?
let rankString = ranking.rank != nil ? "\(ranking.rank!)" : ""
newRankings.append(Person(id: ranking.name, name: ranking.name, ranking: rankString, rankForSort: ranking.rank ?? Int.max)
}
newRankings.sort(by: sort)
people = newRankings
}
}
var body : some View {
Table(people, selection: $selection, sortOrder: $sort) {
TableColumn("Name", value: \Person.name)
TableColumn("Rank", value: \Person.rank)
}
.onChange(of: sortOrder) {
people.sort(using: $0)
}
}
Here is a way to do an initial sort and uses didSet which has the added benefit it doesn't use onChange which results in body called twice when resorting.
struct ProductsData {
var sortedProducts: [Product]
var sortOrder = [KeyPathComparator(\Product.name)] {
didSet {
sortedProducts.sort(using: sortOrder)
}
}
init() {
sortedProducts = myProducts.sorted(using: sortOrder)
}
}
struct ContentView: View {
#State private var data = ProductsData()
var body: some View {
ProductsTable(data: $data)
}
}
struct ProductsTable: View {
#Environment(\.horizontalSizeClass) private var horizontalSizeClass
#Binding var data: ProductsData
var body: some View {
Table(data.sortedProducts, sortOrder: $data.sortOrder) {
See this answer to a different question for the full sample.

How to add a modifier to any specific buttons inside a ForEach loop for an array of buttons in SwiftUI?

How can I add modifiers according to my needs to individual buttons?
I have a ForEach loop running in a range of 0..<5 & I need to add an animation of .rotation3DEffect in clicked button and another .opacity on remaining un-clicked buttons, according to their state.
Note: I can watch for any state value inside buttons action, and then change it to have the animation, but since the modifier is applied on
the Button itself inside the ForEach loop, I am having the animation
applied to every buttons inside the foreach.
#State private var rotationDegree = 0.0
#State private var selectedNum = 0
#State private var correctAnswer = 0 // comes from saved data
var body: some View {
ForEach(0..<5) { num in // image as a button, loops through their prefix name
Button {
selectedNum = (num == correctAnswer) ? num : 0
withAnimation {
// once clicked, will animate all 5 buttons, need only this clicked button to animate.
if (num == correctAnswer) {
rotationDegree += 360
}
}
} label: {
Image("imageName\(num)")
}
.rotation3DEffect((.degrees((selectedNum == correctAnswer) ? rotationDegree : 0.0)), axis: (x: 0, y: (selectedNum == correctAnswer) ? 1 : 0, z: 0)) // animation I want to add to a specific button
}
}
import SwiftUI
struct AnimatedListView: View {
var body: some View {
VStack{
ForEach(0..<5) { num in
//The content of the ForEach goes into its own View
AnimatedButtonView(num: num)
}
}
}
}
struct AnimatedButtonView: View {
//This creates an #State for each element of the ForEach
#State private var rotationDegree = 0.0
//Pass the loops data as a parameter
let num: Int
var body: some View {
Button {
withAnimation {
rotationDegree += 360
}
} label: {
Image(systemName: "person")
Text(num.description)
}
.rotation3DEffect((.degrees(rotationDegree)), axis: (x: 0, y: 1, z: 0))
}
}
struct AnimatedListView_Previews: PreviewProvider {
static var previews: some View {
AnimatedListView()
}
}
Try adding this to your view.
#State var selectedNum: Int = 0
Then change your button to look something like this.
Button {
selectedNum = num
withAnimation {
rotationDegree += 360
}
} label: {
Image("imageName\(num)")
}
// Check if the selected button is equal to the num
.rotation3DEffect((.degrees(selectedNum == num ? rotationDegree : 0.0)), axis: (x: 0, y: 1, z: 0))
Not sure how the rotation3DEffect works. But here is another example
.background(selectedNum == num ? .red : .blue)

Display subview from an array in SwiftUI

I am trying to present a sequence of Views, each gathering some information from the user. When users enter all necessary data, they can move to next View. So far I have arrived at this (simplified) code, but I am unable to display the subview itself (see first line in MasterView VStack{}).
import SwiftUI
protocol DataEntry {
var entryComplete : Bool { get }
}
struct FirstSubView : View, DataEntry {
#State var entryComplete: Bool = false
var body: some View {
VStack{
Text("Gender")
Button("Male") {
entryComplete = true
}
Button("Female") {
entryComplete = true
}
}
}
}
struct SecondSubView : View, DataEntry {
var entryComplete: Bool {
return self.name != ""
}
#State private var name : String = ""
var body: some View {
Text("Age")
TextField("Your name", text: $name)
}
}
struct MasterView: View {
#State private var currentViewIndex = 0
let subview : [DataEntry] = [FirstSubView(), SecondSubView()]
var body: some View {
VStack{
//subview[currentViewIndex]
Text("Subview placeholder")
Spacer()
HStack {
Button("Prev"){
if currentViewIndex > 0 {
currentViewIndex -= 1
}
}.disabled(currentViewIndex == 0)
Spacer()
Button("Next"){
if (currentViewIndex < subview.count-1){
currentViewIndex += 1
}
}.disabled(!subview[currentViewIndex].entryComplete)
}
}
}
}
I do not want to use NavigationView for styling reasons. Can you please point me in the right direction how to solve this problem? Maybe a different approach?
One way to do this is with a Base View and a switch statement combined with an enum. This is a similar pattern I've used in the past to separate flows.
enum SubViewState {
case ViewOne, ViewTwo
}
The enum serves as a way to easily remember and track which views you have available.
struct BaseView: View {
#EnvironmentObject var subViewState: SubViewState = .ViewOne
var body: some View {
switch subViewState {
case ViewOne:
ViewOne()
case ViewTwo:
ViewTwo()
}
}
}
The base view is a Container for the view control. You will likely add a view model, which is recommended, and set the state value for your #EnvironmentObject or you'll get a null pointer exception. In this example I set it, but I'm not 100% sure if that syntax is correct as I don't have my IDE available.
struct SomeOtherView: View {
#EnvironmentObject var subViewState: SubViewState
var body: some View {
BaseView()
Button("Switch View") {
subViewState = .ViewTwo
}
}
}
This is just an example of using it. You can access your #EnvironmentObject from anywhere, even other views, as it's always available until disposed of. You can simply set a new value to it and it will update the BaseView() that is being shown here. You can use the same principle in your code, using logic, to determine the view to be shown and simply set its value and it will update.

Resources