Failing to build SwiftUI with more than 2 ForEach statements - xcode

I'm receiving the following error when I try and build an Xcode project using SwiftUI.
The error I get is "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
I have tried to remove a lot of the complexities of the code to simplify it down as much as possible. I also tried reducing the number of lines of Picker's to test whether that was the issues.
struct ContentView: View {
// set child name
#State private var childName: String = ""
// set the days of the week
let daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
// set empty
#State private var selectedAttendance: String = ""
let attendance = ["None", "All Day", "AM", "PM"]
#State private var mondayAttendance = 0
#State private var tuesdayAttendance = 0
#State private var wednesdayAttendance = 0
#State private var thursdayAttendance = 0
#State private var fridayAttendance = 0
#State private var selectedProvider = 0
let providers = ["School 1", "School 2", "School 3", "School 4", "School 5"]
var body: some View {
NavigationView {
Form {
Section(header: Text("Your Child's Details")) {
TextField("Name", text: $childName)
}
// PICKER to choose childcare provider
Section(header: Text("Who looks after your child?")) {
Picker("Childcare Provider", selection: $selectedProvider) {
ForEach(0 ..< providers.count) {
Text("\(self.providers[$0])")
}
}
}
Section(header: Text("Which days does your child attend?")) {
VStack {
// Monday
HStack {
Text("Monday")
.dayOfWeek()
Picker("Monday", selection: $mondayAttendance) {
ForEach(0 ..< 4) {
Text("\(self.attendance[$0])")
}
}
.pickerStyle(SegmentedPickerStyle())
}
// Tuesday
HStack {
Text("Tuesday")
.dayOfWeek()
Picker("Tuesday", selection: $tuesdayAttendance) {
ForEach(0 ..< 4) {
Text("\(self.attendance[$0])")
}
}
.pickerStyle(SegmentedPickerStyle())
}
//
// // Wednesday
// HStack {
// Text("Wednesday")
// .dayOfWeek()
// Picker("Wednesday", selection: $wednesdayAttendance) {
// ForEach(0 ..< 4) {
// Text("\(self.attendance[$0])")
// }
// }
// .pickerStyle(SegmentedPickerStyle())
// }
}
}
}
.navigationBarTitle("Your Child")
}
}
}
When I try and build with just "Monday" and "Tuesday" showing, it works. As soon as I uncomment Wednesday (or more), the build fails and I get the error.
I understand that I need to simplify the code, but it's pretty simple already and I'm not sure where to take it next.
Any help is much appreciated.

It has nothing to do with For Each. Where is the definition of dayOfWeek() that is attached to Text. It should return a view. It should have failed even with one For Each as it had dayOfWeek(). Probably that modifier might have been defined in your app.

Related

Animation doesn't work on AnyTransition SwiftUI

I'm currently using SwiftUI on a function where a list of images change automatically with opacity animation for each image.
While I've currently managed to get a transition going, the opacity animation does not work no matter what I try.
Could someone please help me out with this...
The code I'm working on is as follows:
//
// EpilogueThree.swift
//
import SwiftUI
struct EpilogueThree: View {
let images = ["1", "2", "3"]
let imageChangeTimer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
let transition = AnyTransition.asymmetric(insertion: .slide, removal: .scale).combined(with: .opacity)
#State private var imagePage = 2
#State private var currentImageIndex = 0
var body: some View {
ZStack {
VStack {
Text("Page: \(imagePage)")
.font(.largeTitle)
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 5)
.background(.black.opacity(0.75))
Image(images[currentImageIndex])
.resizable()
.ignoresSafeArea()
.transition(transition)
.onReceive(imageChangeTimer) { _ in
if imagePage > 0 {
self.currentImageIndex = (self.currentImageIndex + 1) % self.images.count
imagePage -= 1
}
}
}
}
}
}
struct EpilogueThree_Previews: PreviewProvider {
static var previews: some View {
EpilogueThree()
.previewInterfaceOrientation(.landscapeRight)
}
}
The current code acts something like this:
But the code I want needs to do something like this:
We can make image removable in this case by add id (new id - new view) and add animation to container... for animation.
Here is fixed part. Tested with Xcode 13.4 / iOS 15.5
VStack {
// .. other code
Image(images[currentImageIndex])
.resizable()
.ignoresSafeArea()
.id(imagePage) // << here !!
.transition(transition)
.onReceive(imageChangeTimer) { _ in
if imagePage > 0 {
self.currentImageIndex = (self.currentImageIndex + 1) % self.images.count
imagePage -= 1
}
}
}
.animation(.default, value: imagePage) // << here !!

SwiftUI - Transition animation bug (Resolved: not a bug!)

This is a follow up to a prior question which was solved then unsolved.
The situation is that I have a grid of text on a screen that is presented via a transition from another view. I can't use LazyVGrid to present the grid because the width of one column needs to match the longest text in it. So the solution I have is to use HStacks and set the width of the column. To set that width as the width of the longest text I'm using a GeometryReader to read the size of the text and then sending that information via a anchorPreference up the view tree to a #State variable that's used to set the width of all the labels in the column.
It sounds complicated but it works. At least ... until I tried to transition to it. Then an old bug returned where the use of the anchorPreference and onPreferenceChange(...) function seem to change the animations bringing on the view and cause the text to slide on too fast. As per this screen capture:
At the moment I'm at a loss as to how to correct the animations so the text slides on with the parent view. Any suggestions?
Here is the complete code for this bug:
import SwiftUI
#main
struct TransitionAnimationBug: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var displaySettings = false
var body: some View {
Group {
if displaySettings {
DataView(displaySettings: $displaySettings)
.transition(.slide)
} else {
MainView(displaySettings: $displaySettings)
.transition(.slide)
}
}
.animation(.easeInOut, value: displaySettings)
}
}
struct MainView: View {
let displaySettings: Binding<Bool>
var body: some View {
VStack(spacing: 20) {
Button("Show transition bug") {
displaySettings.wrappedValue.toggle()
}
Text("Watch the text as it animates on. it should slide with the view, but instead moves around independently.")
.padding(20).multilineTextAlignment(.center)
Text("This bug is triggered by the label width update via a #State variable in the onPreferenceChange function.")
.padding(20).multilineTextAlignment(.center)
}
}
}
// The preference key used to advise the parent view of a label's width.
struct LabelWidthPreferenceKey: PreferenceKey {
static var defaultValue = 0.0
static func reduce(value: inout Double, nextValue: () -> Double) {
if value != nextValue() {
value = max(value, nextValue())
}
}
}
struct DataView: View {
let displaySettings: Binding<Bool>
#State private var labelWidth: CGFloat = 0.0
var body: some View {
VStack(spacing: 30) {
row(title: "Short title", desc: "Short title long description")
row(title: "Rather long title", desc: "Rather long title long description")
row(title: "SS", desc: "Super short text")
Button("Close") { displaySettings.wrappedValue.toggle() }
}
.onPreferenceChange(LabelWidthPreferenceKey.self) {
// Updating the label width here triggers the bug.
if $0 != labelWidth {
labelWidth = $0
}
}
}
private func row(title: String, desc: String) -> some View {
GeometryReader { geometry in
HStack(alignment: .center) {
Text(title)
.frame(minWidth: labelWidth, alignment: .leading)
.border(.red)
.anchorPreference(key: LabelWidthPreferenceKey.self, value: .bounds) {
geometry[$0].width.rounded(.up)
}
Text(desc)
.border(.red)
}
}
.fixedSize(horizontal: false, vertical: true)
.padding([.leading, .trailing], 20)
}
}
Not a SwiftUI bug. Find below a fix (tested with Xcode 13.3 / iOS 15.4)
VStack(spacing: 30) {
row(title: "Short title", desc: "Short title long description")
row(title: "Rather long title", desc: "Rather long title long description")
row(title: "SS", desc: "Super short text")
Button("Close") { displaySettings.wrappedValue.toggle() }
}
.animation(nil, value: labelWidth) // << here !!
.onPreferenceChange(LabelWidthPreferenceKey.self) {

SwiftUI Picker: The compiler is unable to type-check this expression in reasonable time

folders are fetched first and they are listed in the second picker(whose selection is $folderSelection), and the selected folder's childlists are shown in the third picker(whose selection is $listSelection.
When I compiled this code, Xcode omits an error(The compiler is unable to type-check this expression in reasonable time)
By commenting out each block of code, I figured out that the third picker caused the problem, maybe because it is too complicated.
Picker("Choose List", selection: $listSelection) {
ForEach(Array(folderSelection?.childlists?.allObjects)) { index in
Text(Array(folderSelection!.childlists!.allObjects)[index].name ?? "")
.tag(Array(folderSelection!.childlists!.allObjects)[index] as ListStruct?)
}
}
I tried to fetch folders and lists by adding init(), but then folderSelection variable becomes problem.
Update on April 2
I also tried using .onRecieve for folderSelection Picker, like this
Picker("Choose Folder", selection: $folderSelection) {
ForEach(folders) { (folder: FolderStruct) in
Text(folder.name ?? ""
.tag(folder as FolderStruct?)
}
.onReceive([self.folderSelection].publisher.first()) { (selectedFolder) in
lists = selectedFolder?.childlists?.allObjects as! [ListStruct]
}
}
But when I run on Simulator, it omits an error
Thread 1: Swift runtime failure: force unwrapped a nil value
Is there any solution for this?
Full code
import SwiftUI
import CoreData
struct QuizHomeView: View {
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \FolderStruct.name, ascending: true)],
animation: .default)
private var folders: FetchedResults<FolderStruct>
#State private var showSheet = false
#State private var quizType = "Guess the meaning"
#State private var folderSelection: FolderStruct?
#State private var listSelection: ListStruct?
let quizTypes = ["Guess the meaning", "Guess the word"]
var body: some View {
NavigationView {
Form {
Section {
Picker("Quiz Type", selection: $quizType) {
ForEach(["Guess the meaning", "Guess the word"], id: \.self) {
Text(LocalizedStringKey($0))
}
}
.pickerStyle(.segmented)
}
Section {
Picker("Choose Folder", selection: $folderSelection) {
ForEach(folders, id: \.self) { (folder: FolderStruct) in
Text(folder.name ?? "")
.tag(folder as FolderStruct?)
}
}
Picker("Choose List", selection: $listSelection) {
ForEach(Array(folderSelection?.childlists?.allObjects).indices, id: \.self) { index in
Text(Array(folderSelection!.childlists!.allObjects).indices[index].name ?? "")
.tag(Array(folderSelection!.childlists!.allObjects).indices[index] as ListStruct?)
}
}
}
Section {
Button("Start") {
showSheet.toggle()
}
.disabled(folderSelection == nil || listSelection == nil)
}
}
.navigationTitle("Quiz")
.sheet(isPresented: $showSheet) {
QuizPlayView()
}
}
}
}
Entities and Relationships of FolderStruct and ListStruct

Dealing cards from a deck - MatchedGeometryEffect - Multiple Inserted Views

I'm trying to deal groups of cards from a deck, and have the cards fly from the deck to their respective spots on screen (Stanford 2021 IOS class SET game). I'm tracking the status of each card, including whether it has been dealt or not. I thought I had all the logic correct, but the animation using a matchedGeometryEffect is not working. Both in the canvas view and when running the application in the simulator, the cards just go to their final spots in the overall View, and do not animate individually.
In an earlier version, I was seeing multiple debugger messages about "Multiple inserted views in matched geometry group Pair<Int, ID> have 'isSource: true'", but those messages no longer appear. With this slimmed-down version, the cards simply fail to animate between their original positions (in the card deck) and their final positions in the VGrid.
This codes uses a modified LazyVGrid to display the cards, with a control called AspectVGrid. The primary difference between the original LazyVGrid and the AspectVGrid is that the AspectVGrid creates a LazyVGrid with a fixed aspect ratio. The AspectVGrid only controls the grid layout itself. The code for that struct is included below.
When the application starts, all the cards are available in the deck, and each card's view is assigned its source matchedGeometryEffect. The cards are all marked as undealt (in a #State Set of card IDs). When the deck is tapped (onTapGesture), the model updates either twelve or three additional cards as wasDealt, and those newly dealt cards are supposed to be animated via a "withAnimation" code block, with individual Views in the AspectVGrid as their destinations.
Any suggestions on how to resolve this would be welcome. This seems like a pretty straightforward process, but I am clearly missing something in my implementation.
Thanks in advance for any ideas on this. I've included the model, View Model, and Views below.
Model
import Foundation
import SwiftUI
struct dataModel {
struct Card: Identifiable {
var wasDealt: Bool
var wasDiscarded: Bool
var dealDelay: Double
let id: Int
}
private(set) var firstCardWasDealt: Bool = false
private(set) var cards: Array<Card>
var cardsDealt: Array<Card> {
get {cards.filter({ card in card.wasDealt})}
}
private var numberOfCardsInPlay: Int {
get { cardsDealt.count }
}
var cardsDisplayed: Array<Card> {
get {cardsDealt.filter({ card in !card.wasDiscarded })}
}
var cardsInDeck: Array<Card> {
get {cards.filter({ card in !card.wasDealt })}
}
init() {
cards = []
newGame()
}
// Divides the total time to deal the cards by the number of cards to deal,
// and provides the applicable delay to the nth (index) card in the group of
// cards to be dealt.
private func calcDelay(numCards: Int, index: Int, totalDelay: Double) -> Double {
return Double(index) * (totalDelay / Double(numCards))
}
// If no cards have been dealt, deal twelve cards. Otherwise, deal three cards.
// When dealing the cards, apply the appropriate delay to each card being dealt.
// If all cards are already in play, don't deal any more cards.
mutating func dealMoreCards() {
if !firstCardWasDealt {
for index in (0...11) {
cards[index].dealDelay = calcDelay(numCards: 12, index: index, totalDelay: CardConstants.total12CardDealDuration)
cards[index].wasDealt = true
}
firstCardWasDealt = true
} else {
if numberOfCardsInPlay < cards.count {
let startIndex = numberOfCardsInPlay
for index in (startIndex...(startIndex + 2)) {
cards[index].dealDelay = calcDelay(numCards: 3, index: index, totalDelay: CardConstants.total3CardDealDuration)
cards[index].wasDealt = true
}
}
}
}
mutating func newGame() {
firstCardWasDealt = false
cards = []
for index in (0...80) {
cards.append(Card(wasDealt: false, wasDiscarded: false, dealDelay: 0, id: index))
}
}
}
struct CardConstants {
static let color = Color.red
static let aspectRatio: CGFloat = 2/3
static let dealAnimationDuration: Double = 0.2 // 0.5 - This value controls how long it takes to animate each card
static let total12CardDealDuration: Double = 3.0 // this controls how long it takes to deal twelve cards (in seconds)
static let total3CardDealDuration: Double = 0.75 // this controls how long it takes to deal three cards (in seconds)
}
View Model
import SwiftUI
class ViewModel: ObservableObject {
#Published private var model: dataModel
init() {
model = dataModel()
}
var cardsDealt: Array<dataModel.Card> {
model.cardsDealt
}
var cardsDisplayed: Array<dataModel.Card> {
model.cardsDisplayed
}
var cardsInDeck: Array<dataModel.Card> {
model.cardsInDeck
}
func choose(_ card: dataModel.Card) {
// do something with the chosen card
}
func dealMoreCards() {
model.dealMoreCards()
}
}
View
import SwiftUI
struct exampleView: View {
#ObservedObject var example: ViewModel
#Namespace private var dealingNamespace
#State private var dealt = Set<Int>()
private func deal(_ card: dataModel.Card) {
dealt.insert(card.id)
}
private func isUnDealt(_ card: dataModel.Card) -> Bool {
!dealt.contains(card.id)
}
var body: some View {
VStack {
Text("Example - \(example.cardsDisplayed.count) cards Displayed")
AspectVGrid(items: example.cardsDisplayed, aspectRatio: 2/3, content: { card in CardView(card: card)
.padding(4)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
.transition(AnyTransition.asymmetric(insertion: .opacity, removal: .opacity))
.onTapGesture {
example.choose(card)
}
})
HStack {
deckBody
Spacer()
}
}
}
var deckBody: some View {
ZStack {
ForEach(example.cardsInDeck) {
card in CardView(card: card)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
.transition(AnyTransition.asymmetric(insertion: .opacity, removal: .identity))
}
}
.frame(width: 60, height: 90)
.foregroundColor(.cyan)
.onTapGesture {
example.dealMoreCards() // Make the next group of cards available (12 or 3)
// deal cards
for card in example.cardsDealt {
if isUnDealt(card) {
withAnimation(Animation.easeInOut(duration: CardConstants.dealAnimationDuration).delay(card.dealDelay)) {
deal(card)
}
}
}
}
}
}
struct CardView: View {
let card: dataModel.Card
var body: some View {
VStack {
ZStack {
let shape = RoundedRectangle(cornerRadius: 5)
if card.wasDealt {
shape.fill().foregroundColor(.white)
} else {
shape.fill().foregroundColor(.cyan)
}
shape.strokeBorder(lineWidth: 3)
Text("Card: \(card.id)")
.padding(4)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let example = ViewModel()
exampleView(example: example)
}
}
AspectVGrid
//
// AspectVGrid.swift
//
// Created by CS193p Instructor on 4/14/21.
// Copyright Stanford University 2021
//
import SwiftUI
struct AspectVGrid<Item, ItemView>: View where ItemView: View, Item: Identifiable {
var items: [Item]
var aspectRatio: CGFloat
var content: (Item) -> ItemView
init(items: [Item], aspectRatio: CGFloat, #ViewBuilder content: #escaping (Item) -> ItemView) {
self.items = items
self.aspectRatio = aspectRatio
self.content = content
}
var body: some View {
GeometryReader { geometry in
VStack {
let width: CGFloat = widthThatFits(itemCount: items.count, in: geometry.size, itemAspectRatio: aspectRatio)
LazyVGrid(columns: [adaptiveGridItem(width: width)], spacing: 0) {
ForEach(items) { item in
content(item).aspectRatio(aspectRatio, contentMode: .fit)
}
}
Spacer(minLength: 0)
}
}
}
private func adaptiveGridItem(width: CGFloat) -> GridItem {
var gridItem = GridItem(.adaptive(minimum: width))
gridItem.spacing = 0
return gridItem
}
private func widthThatFits(itemCount: Int, in size: CGSize, itemAspectRatio: CGFloat) -> CGFloat {
var columnCount = 1
var rowCount = itemCount
repeat {
let itemWidth = size.width / CGFloat(columnCount)
let itemHeight = itemWidth / itemAspectRatio
if CGFloat(rowCount) * itemHeight < size.height {
break
}
columnCount += 1
rowCount = (itemCount + (columnCount - 1)) / columnCount
} while columnCount < itemCount
if columnCount > itemCount {
columnCount = itemCount
}
return floor(size.width / CGFloat(columnCount))
}
}
MatchedGeometryEffectApp
import SwiftUI
#main
struct MatchedGeometryEffectApp: App {
private let example = ViewModel()
var body: some Scene {
WindowGroup {
exampleView(example: example)
}
}
}
So, the problem you have been having with the matched geometry seems to be that AspectVGrid, which you were given as part of the assignment, interferes with the matched geometry animation. I suspect it has to do with the resizing mechanism. Removing that gives you a matched geometry that does what one would expect.
Here is a working example of the matched geometry in your ExampleView. I also rewrote CardView to make it simpler.
struct ExampleView: View {
#ObservedObject var example: ViewModel
#Namespace private var dealingNamespace
#State private var dealt = Set<Int>()
private func deal(_ card: DataModel.Card) {
dealt.insert(card.id)
}
private func isUnDealt(_ card: DataModel.Card) -> Bool {
!dealt.contains(card.id)
}
let columns = [
GridItem(.adaptive(minimum: 80))
]
var body: some View {
VStack {
VStack {
Text("Example - \(example.cardsDisplayed.count) cards Displayed")
LazyVGrid(columns: columns, spacing: 10) {
ForEach(example.cardsDisplayed) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
.onTapGesture {
example.choose(card)
}
.padding(4)
}
}
Spacer()
HStack {
deckBody
Spacer()
}
}
}
}
var deckBody: some View {
ZStack {
ForEach(example.cardsInDeck) { card in
CardView(card: card)
.matchedGeometryEffect(id: card.id, in: dealingNamespace)
}
}
.frame(width: 60, height: 90)
.foregroundColor(.cyan)
.onTapGesture {
withAnimation {
example.dealMoreCards() // Make the next group of cards available (12 or 3)
// deal cards
for card in example.cardsDealt {
if isUnDealt(card) {
deal(card)
}
}
}
}
}
}
struct CardView: View {
let card: DataModel.Card
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill()
.foregroundColor(card.wasDealt ? .white : .cyan)
RoundedRectangle(cornerRadius: 5)
.strokeBorder(lineWidth: 3)
Text("Card: \(card.id)")
.padding(4)
}
}
}
A quick note about naming conventions. Types and structs should all be capitalized, so ExampleView and DataModel would be convention, and I changed the code to reflect that. You will need to fix yours when you incorporate this code.
Also note that this view isn't perfect. Eventually, it will push the deck of cards off the bottom of the screen, but that can be handled.
Your attempt to make the cards fly one at a time doesn't work. First, the withAnimation has to capture example.dealMoreCards() and trying to implement a .delay in that situation doesn't work. It can be implemented with a DispatchQueue.main.asyncAfter(deadline:), but you would have to rework how you pass the timing as the individual cards won't be available yet.
Lastly, the transitions do nothing in this situation.

How to create a dynamic calculation based off more than 1 State

I am brand new to Swift (and coding in general). I am working on an app that will output a calculation based off the tracking of two states. The two states are brewModel and waterAmount. I am able to successfully create a function that will return one calculation based on the two states. However, now I am trying to create a Picker that will toggle the calculation between two measurements - grams and tablespoons. This is where I am having trouble.
I tried to write a series of conditionals in different ways such as if and else if as well as switch cases, but it doesn't work. When I build the simulator, Xcode will just think for a long time until I stop it. Sometimes I get error messages after I manually stop it and sometimes I don't. Today I got "Command CompileSwiftSources failed with a nonzero exit code."
Does anyone have any suggestions?
Also, I apologize if my code is messy, I have a bunch of things commented out that I am playing with. The func computeGrinds does work but just for the one calculation. Thank you!
import SwiftUI
struct Water: View {
// #EnvironmentObject var favorites: Favorites
#State var animationInProgress = true
#State var brewModel: BrewModel
#State var waterAmount: Int = 1
#State var grindsSelection = "tbsp"
var grindOptions = ["tbsp", "grams"]
// var resultGrindCalc: Double {
//
// var value = Double(0)
// }
// switch grindsSelection {
// case "tbsp" || brewModel.frenchPress:
// value = Double(waterAmount) * 2.5
//
// }
//
// func computeGrinds () -> Double {
// switch brewModel {
// case .frenchPress, .chemex:
// return (2.5 * Double(waterAmount))
// case .drip :
// return Double(2 * Double(waterAmount))
// case .mokaPot:
// return Double(1 * Double(waterAmount))
// case .aeroPress:
// return Double(1.6 * Double(waterAmount))
// // default:
// // return(1 * Double(waterAmount))
// }
// }
var body: some View {
VStack (spacing: 5) {
Spacer()
HStack {
// Text("").padding(20)
Text("How many cups do you want to brew?")
Picker("", selection: $waterAmount) {
ForEach(1...15, id: \.self){
Text("\($0)")
}
}
// Spacer()
}.padding()
.overlay (
RoundedRectangle(cornerRadius: 16)
.stroke(Color("Custom Color"), lineWidth: 8)
)
// gif/image conditionals
if (brewModel == .frenchPress) {
LottieView(name: "frenchpress", loopMode: .loop)
} else if brewModel == .chemex {
LottieView(name: "pourover", loopMode: .loop)
} else if brewModel == .aeroPress {
LottieView(name: "aeropress", loopMode: .loop)
} else if brewModel == .mokaPot {
LottieView(name: "mokapot", loopMode: .loop)
} else if brewModel == .drip {
Image("Drip")
.resizable()
.scaledToFit()
}
// I would have more conditionals but testing with just these two for now
var testingCalcCond = Double
if (brewModel == .frenchPress)||(grindsSelection=="tbsp") {
testingCalcCond = (2.5 * Double(waterAmount))
} else if (brewModel == .frenchPress)||(grindsSelection=="grams") {
testingCalcCond = (16 * Double(waterAmount))
}
let formatted = String(format: "%.2f", testingCalcCond)
// let formatted = String(format: "%.2f", computeGrinds())
HStack {
Text("**\(formatted)**")
Picker("Select Grinds Units: ", selection: $grindsSelection, content: {
ForEach(grindOptions, id: \.self) {
Text($0)
}
}).onChange(of: grindsSelection) { _ in computeGrinds() }
Text("of coffee grinds needed")
}
.padding()
.overlay (
RoundedRectangle(cornerRadius: 16)
.stroke(Color("Custom Color"), lineWidth: 8)
)
}
Spacer()
}
}
struct Water_Previews: PreviewProvider {
static var previews: some View {
Water(brewModel: .drip)
}
}
}
*I'm using Xcode 13.2.1
*I'm using swiftUI
There are 2 aspects you want to think over:
1. How to update the result value based on the inputs.
Your result value is based on two inputs: brewModel and waterAmount. Both are #State vars and changed by a picker.
I changed your computeGrinds func to a computed property, because this will be automatically called when one of the two base values changes. Then there is no need for .onchange anymore, you can just use the var value – it will always be up to date.
2. recalculating from tbsp to grams.
This is more of a math thing: As I understand, for .frenchPress you need either 2.5 tbsp – or 16 grams per cup. So 1 tbsp = 16 / 2.5 = 6.4 grams. Once you know that you just have to go through the switch case once, and use the unitValue to recalculate. I integrated that too ;)
Here is my simplified code:
enum BrewModel {
case frenchPress
case chemex
case drip
case mokaPot
case aeroPress
}
struct ContentView: View {
#State var animationInProgress = true
#State var brewModel: BrewModel = .frenchPress
#State var waterAmount: Int = 1
#State var grindsSelection = "tbsp"
let grindOptions = ["tbsp", "grams"]
// computed var instead of func, does the same
var computeGrinds: Double {
// transforms tbsp = 1 to grams (= 6.4 ?)
var unitValue: Double = 1.0
if grindsSelection == "grams" {
unitValue = 6.4
}
switch brewModel {
case .frenchPress, .chemex:
return (2.5 * unitValue * Double(waterAmount))
case .drip :
return Double(2 * unitValue * Double(waterAmount))
case .mokaPot:
return Double(1 * unitValue * Double(waterAmount))
case .aeroPress:
return Double(1.6 * unitValue * Double(waterAmount))
}
}
var body: some View {
VStack (spacing: 5) {
HStack {
Text("How many cups do you want to brew?")
Picker("", selection: $waterAmount) {
ForEach(1...15, id: \.self){
Text("\($0)")
}
}
}
.padding()
.overlay (
RoundedRectangle(cornerRadius: 16)
.stroke(Color.brown, lineWidth: 8)
)
.padding(.bottom)
let formatted = String(format: "%.2f", computeGrinds)
HStack {
Text("**\(formatted)**")
Picker("Select Grinds Units: ", selection: $grindsSelection, content: {
ForEach(grindOptions, id: \.self) {
Text($0)
}
})
Text("of coffee grinds needed")
}
.padding()
.overlay (
RoundedRectangle(cornerRadius: 16)
.stroke(Color.brown, lineWidth: 8)
)
}
}
}

Resources