onDelete: Accessing Data From Entry Before it is Deleted - ios14

When a user swipes to delete an entry in a list, isn't there a pointer that can be used to get information on the entry being deleted? Before the entry is deleted I want to know the category total. I'm having trouble with the function removeItems which is giving the error: Cannot convert value of type 'IndexSet' to expected argument type 'Int'
struct CatItem: Codable, Identifiable {
var id = UUID()
var catNum: Int
var catName: String
var catTotal: Double
var catPix: String
var catShow: Bool
}
class Categories: ObservableObject {
#Published var catItem: [CatItem] {
didSet {
}
}
}
struct manCatView: View {
#EnvironmentObject var userData: UserData
#EnvironmentObject var categories: Categories
var pic: String = ""
var body: some View {
List {
ForEach(0 ..< categories.catItem.count) { index in
if index.catShow == true {
HStack {
let pic = categories.catItem[index].catPix
Image(systemName: pic)
.resizable()
.foregroundColor(Color(colors[index]))
.frame(width: 40, height: 40)
Text(categories.catItem[index].catName)
}
}
}
.onDelete(perform: removeItems)
}
.navigationBarTitle(Text("Manage Categories"), displayMode: .inline)
NavigationLink(destination: addCatView()) {Text("Add Categories")
.fontWeight(.bold)}
.font(.title2)
.disabled(categories.catItem.count > 16)
.padding([.leading, .trailing], 10)
.accentColor(Color(red: 60/255, green: 160/255, blue: 240/255))
Text("")
Text("")
Text("Add Up to 5 Categories")
Text("Swipe Left to Delete a Category")
Rectangle()
.frame(width: 50, height: 175)
.foregroundColor(.white)
}
func removeItems(at offsets: IndexSet) {
var catTotal: Double = 0.0
//subtract entry amount from category total
catTotal = categories.catItem[offsets].catTotal // <-- need help with index here
userData.totalArray[grandTotal] -= catTotal
userData.totalArray[userTotal] -= catTotal
categories.catItem.remove(atOffsets: offsets)
}
}

IndexSet here is simple Set of Indexies, in simple case it will contain only one element (user delete entries one by one with swipe). So for simplicity you can use .first element for this. But better - to iterate through full set, it'll allow you more control.
so code you need may look similar to this:
func removeItems(at offsets: IndexSet) {
var catTotal: Double = 0.0
offsets.forEach { singleOffset in
//subtract entry amount from category total
catTotal = categories.catItem[singleOffset].catTotal
userData.totalArray[grandTotal] -= catTotal
userData.totalArray[userTotal] -= catTotal
}
categories.catItem.remove(atOffsets: offsets)
}

Related

SwiftUI code too long for the compiler to type-check it, but I can't seem to break it down into parts

I am trying to make a view iteratively from a JSON file fetched from a PHP URL. The view is made with a for each loop to create an information box for every different station in the JSON and also makes them a navigation link. Ideally, I would tap the box to get more information on a specific tide station (all station data hasn't been presented on the view yet).
Since I am using a for each loop, I represent the current station number in the data array with i. In order to get the same data as the box I click I use the same i number for other relevant data.
My issue is that as I am building everything in the same for each loop to keep that number i in the scope, my code gets too long for the compiler to check. I heard that this could happen even if the code wasn't too long but simply for a typing mistake somewhere, but I have yet to find a mistake that breaks the code and truly believe it is due to the length as it was working if I commented-out some parts.
To resolve that I understand I need to break my code into different sections for the compiler to check them individually so as not to start the process every time.
However, as I am building everything into the for each loop to have the i number in the scope, I cannot make sub-views that use that number.
I am not entirely sure this is the question that would best solve my issue, but how can I pass the for each loop parameter into another view/function/or something else?
I apologize for the code being very rough and not following good coding practices, if not just for SwiftUI, I am quite inexperienced with programming in general. I left the entirety of the code to be sure not to leave any possibility out.
import Foundation
import SwiftUI
import Alamofire
import SwiftyJSON
import MapKit
public var array_tides_site_name = [String]()
public var array_tides_next_lw_time = [String]()
public var array_tides_next_lw_height = [Double]()
public var array_tides_next_hw_time = [String]()
public var array_tides_next_hw_height = [Double]()
public var array_tides_tidal_state = [String]()
public var array_tides_latitude = [Double]()
public var array_tides_longitude = [Double]()
public var array_tides_observed_height = [Double]()
public var array_tides_predicted_height = [Double]()
public var array_tides_surge = [Double]()
struct Previews_GeneralTides_Previews: PreviewProvider {
static var previews: some View {
GeneralTidesView()
}
}
struct GeneralTidesView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack (alignment: .center) {
Spacer()
Image(systemName: "chart.xyaxis.line")
.font(.largeTitle)
.frame(width: 300, height: 300, alignment: .center)
Spacer()
Text("More Stations")
.font(.title3)
.foregroundStyle(LinearGradient(colors: [.primary, .secondary], startPoint: .topLeading, endPoint: .bottomTrailing))
.frame(width: screenWidth, height: 40)
.background(.thickMaterial)
Spacer()
Divider()
.onAppear() {loadStationData()}
Spacer()
StationList()
}
.navigationBarTitle("Tides")
.navigationBarTitleDisplayMode(.inline)
}
}
}
func loadStationData(){
let generalTideUrl = "http://www.pla.co.uk/hydrographics/ajax/ltoverview.php"
AF.request(generalTideUrl, method: .get).responseJSON(){ (generalTideResponse) in
switch generalTideResponse.result {
case .success:
//print(generaltideresponse.result)
let generalTideResult = try? JSON(data: generalTideResponse.data!)
//print(generaltideresult)
//print(generalTideResult!["tides"])
let generalTideArray = generalTideResult!["tides"]
array_tides_site_name.removeAll()
array_tides_next_lw_time.removeAll()
array_tides_next_lw_height.removeAll()
array_tides_next_hw_time.removeAll()
array_tides_next_hw_height.removeAll()
array_tides_tidal_state.removeAll()
array_tides_latitude.removeAll()
array_tides_longitude.removeAll()
array_tides_observed_height.removeAll()
array_tides_predicted_height.removeAll()
array_tides_surge.removeAll()
for i in generalTideArray.arrayValue {
//print(i)
let site_name = i["site_name"].stringValue
array_tides_site_name.append(site_name)
var next_lw_time = i["next_lw_time"].stringValue
let lwRange = next_lw_time.startIndex..<next_lw_time.index(next_lw_time.startIndex, offsetBy: 11)
next_lw_time.removeSubrange(lwRange)
array_tides_next_lw_time.append(next_lw_time)
let next_lw_height = i["next_lw_height"].doubleValue
array_tides_next_lw_height.append(next_lw_height)
var next_hw_time = i["next_hw_time"].stringValue
let hwRange = next_hw_time.startIndex..<next_hw_time.index(next_hw_time.startIndex, offsetBy: 11)
next_hw_time.removeSubrange(hwRange)
array_tides_next_hw_time.append(next_hw_time)
let next_hw_height = i["next_hw_height"].doubleValue
array_tides_next_hw_height.append(next_hw_height)
let tidal_state = i["tidal_state"].stringValue
array_tides_tidal_state.append(tidal_state)
let latitude = i["latitude"].doubleValue
array_tides_latitude.append(latitude)
let longitude = i["longitude"].doubleValue
array_tides_longitude.append(longitude)
let predictedHeight = i["predicted_height"].doubleValue
array_tides_predicted_height.append(predictedHeight)
let observedHeight = i["observed_height"].doubleValue
array_tides_observed_height.append(observedHeight)
let surge = i["surge_height"].doubleValue
array_tides_surge.append(surge)
}
break
case .failure:
print(generalTideResponse.error!)
break
}
}.resume()
}
struct StationList: View {
#State private var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 51.5, longitude: -0.12), span: MKCoordinateSpan(latitudeDelta: 0.6, longitudeDelta: 0.6))
var body: some View {
ForEach(0..<array_tides_site_name.count) { i in
NavigationLink(destination: Group{
VStack(alignment: .center) {
stationNavLink()
Text(array_tides_site_name[i])
.font(.largeTitle)
Map(coordinateRegion: $mapRegion, annotationItems: [Location(name: array_tides_site_name[i], coordinate: CLLocationCoordinate2D(latitude: array_tides_latitude[i], longitude: array_tides_longitude[i]))]) { location in
MapMarker(coordinate: location.coordinate)}
.frame(width: screenWidth, height: 250)
HStack {
RoundedRectangle(cornerRadius: 10)
.foregroundStyle(.thinMaterial)
.frame(width: 150, height: 120)
.overlay {
VStack(alignment: .leading , spacing: 10) {
Text("Next Low Tide:")
HStack {Text("Time: "); Text(array_tides_next_lw_time[i])}.foregroundColor(.secondary)
HStack {Text("Height: "); Text(array_tides_next_lw_height[i].description); Text("m")}.foregroundColor(.secondary)
}
}
RoundedRectangle(cornerRadius: 10)
.foregroundStyle(.thinMaterial)
.frame(width: 150, height: 120)
.overlay {
VStack(alignment: .leading, spacing: 10) {
Text("Next High Tide:")
HStack {Text("Time: "); Text(array_tides_next_hw_time[i])}.foregroundColor(.secondary)
HStack {Text("Height: "); Text(array_tides_next_hw_height[i].description); Text("m")}.foregroundColor(.secondary)
}
}
}
Text(array_tides_surge[i].description)
}
}){
ZStack {
RoundedRectangle(cornerRadius: 8)
.strokeBorder(.white.opacity(0.3), lineWidth: 1)
.background(RoundedRectangle(cornerRadius: 8).fill(.thinMaterial))
.frame(width: screenWidth - 40, height: 80)
.overlay() {
VStack(alignment: .leading) {
Spacer()
Text(array_tides_site_name[i])
.padding(.leading, 10)
.foregroundStyle(LinearGradient(colors: [.secondary, .secondary.opacity(0.8)], startPoint: .leading, endPoint: .trailing))
Spacer()
Divider()
Spacer()
Group {
HStack(){
Spacer()
Text("High Water: ")
Text(array_tides_next_hw_time[i])
Spacer()
Text("Low Water: ")
Text(array_tides_next_lw_time[i])
Spacer()
if array_tides_tidal_state[i] == "Flood" { Image(systemName: "arrow.up").foregroundColor(.green) }
else { Image(systemName: "arrow.down").foregroundColor(.red) }
Spacer()
}
}
Spacer()
}
}
}
}
}
}
}
var stationNavLink: some View {
Text(array_tides_surge[currentStation])
}
struct Location: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
}
Having multiple, associated, arrays to hold your data is a definite code smell. You risk having data get out of sync between the arrays and as you can see, loading and accessing data involves a lot of code.
You can start by creating some [Codable] structs to hold you data. quicktype.io can do this for. Simply paste in your JSON and you get the structs you need.
I have modified Tide to conform to Hashable and Identifiable to make it easier to use in SwiftUI lists.
Also note the use of CodingKeys to convert the properties to Swift naming conventions.
// MARK: - TideData
struct TideData: Codable {
let timezone: String
let tides: [Tide]
}
// MARK: - Tide
struct Tide: Codable, Hashable, Identifiable {
let siteID, siteStation: Int
let siteName: String
let latitude, longitude: Double
let received: String
let observedHeight, predictedHeight, surgeHeight: Double
let nextLwTime: String
let nextLwHeight: Double
let nextHwTime: String
let nextHwHeight: Double
let tidalState: String
var id: Int {
return siteID
}
enum CodingKeys: String, CodingKey {
case siteID = "site_id"
case siteStation = "site_station"
case siteName = "site_name"
case latitude, longitude
case received = "Received"
case observedHeight = "observed_height"
case predictedHeight = "predicted_height"
case surgeHeight = "surge_height"
case nextLwTime = "next_lw_time"
case nextLwHeight = "next_lw_height"
case nextHwTime = "next_hw_time"
case nextHwHeight = "next_hw_height"
case tidalState = "tidal_state"
}
}
Now you can move your loading code into its own class. Make this an ObservableObject and have it publish the tide data.
Since our structs conform to Codable we can use AlamoFire's inbuilt JSON decoding and transformation
lass Loader:ObservableObject {
#Published var tides: [Tide] = []
func load() {
let generalTideUrl = "https://www.pla.co.uk/hydrographics/ajax/ltoverview.php"
AF.request(generalTideUrl, method: .get).responseDecodable(of: TideData.self){ (generalTideResponse) in
switch generalTideResponse.result {
case .success(let tideData):
self.tides = tideData.tides
print(self.tides)
case .failure(let error):
print(error)
}
}
}
}
Much more succinct!
Finally, we can use this data in a SwiftUI view
I have simplified your views for the sake of this answer, but I am sure you will get the idea.
struct GeneralTidesView: View {
#ObservedObject var loader: Loader
var body: some View {
NavigationView {
List {
ForEach(loader.tides) { station in
NavigationLink(destination: StationView(station:station)) {
Text(station.siteName)
}
}
}
}.onAppear {
self.loader.load()
}
}
}
struct Previews_GeneralTides_Previews: PreviewProvider {
static var previews: some View {
GeneralTidesView(loader: Loader())
}
}
struct StationView: View {
var station: Tide
var body: some View {
Form {
HStack {
Text("Predicted Height")
Text("\(station.observedHeight)")
}
HStack {
Text("Predicted Height")
Text("\(station.predictedHeight)")
}
}.navigationTitle(station.siteName)
}
}
You can see how having a struct with properties makes it much easier to pass data around. Also, since all of the properties are in a single struct, we no longer need to work about array indices.

How to make a 5 stars review widget with an incremental of 0.5 in SwiftUI

I have the widget fully working with the 5 stars, the data binding and the interface as follows:
import SwiftUI
struct mainView: View {
#State var rating: Double = 0.0
var body: some View {
VStack {
RatingView(rating: $rating)
Text("\(String(format: "%.1f", rating))")
.font(.system(size: 30))
}.background(.mint)
}
}
struct RatingView: View {
#Binding var rating: Double
var offColor = Color.white
var onColor = Color("AccentColor")
var starHalf = Image(systemName: "star.leadinghalf.filled")
var body: some View {
HStack {
ForEach(1..<6) { number in
Image(systemName: "star.fill")
.foregroundColor(number > Int(self.rating) ? self.offColor : self.onColor)
.onTapGesture {
self.rating = Double(number)
selectionChanged()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
mainView()
}
}
Works perfect, but have no idea how can I implement to have a 0.5 increment, and to change the icon for the half star. there is nothing like .onHelfLeftTapGesture() I can use?
You can't do it reasonably with a tap gesture, unless you want to go a half star at a time. But using a different mechanism will make it flow better. Simply use a Slider with an .opacity(0.1) so it works, but is transparent, in an .overlay() that is keyed to the stars:
struct RatingsView: View {
let ratingsArray: [Double]
let color: Color
#Binding var rating: Double
init(rating: Binding<Double>, maxRating: Int = 5, starColor: Color = .yellow) {
_rating = rating
ratingsArray = Array(stride(from: 0.0, through: Double(max(1, maxRating)), by: 0.5))
color = starColor
}
var body: some View {
VStack {
HStack(spacing: 0) {
ForEach(ratingsArray, id: \.self) { ratingElement in
if ratingElement > 0 {
if Int(exactly: ratingElement) != nil && ratingElement <= rating {
Image(systemName: "star.fill")
.foregroundColor(color)
} else if Int(exactly: ratingElement) == nil && ratingElement == rating {
Image(systemName: "star.leadinghalf.fill")
.foregroundColor(color)
} else if Int(exactly: ratingElement) != nil && rating + 0.5 != ratingElement {
Image(systemName: "star")
.foregroundColor(color)
}
}
}
}
.overlay(
Slider(value: $rating, in: 0.0...ratingsArray.last!, step: 0.5)
.tint(.clear)
.opacity(0.1)
)
}
.onAppear {
rating = Int(exactly: rating) != nil ? rating : Double(Int(rating)) + 0.5
}
}
}
No, I didn't just come up with this, but had come up with this before Christmas to play with the idea. This is the code I came up with.

SwiftUI: How to select multi items(image) with ForEach?

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.

Swift UI | Textfield not reading entered value

I have a textfield which is supposed to log the units of a food product someone has eaten, which is then used to calculate the total number of calories, protein, etc. that the user consumed. But when the value is entered on the textfield, the units variable isn't updated. How can I fix this?
This is my code:
#State var selectFood = 0
#State var units = 0
#State var quantity = 1.0
#State var caloriesInput = 0.0
#State var proteinInput = 0.0
#State var carbsInput = 0.0
#State var fatsInput = 0.0
var body: some View {
VStack {
Group {
Picker(selection: $selectFood, label: Text("What did you eat?")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.white))
{
ForEach(database.productList.indices, id: \.self) { i in
Text(database.productList[i].name)
}
}
.pickerStyle(MenuPickerStyle())
Spacer(minLength: 25)
Text("How much did you have?")
.font(.headline)
.fontWeight(.bold)
.foregroundColor(.white)
.frame(alignment: .leading)
//Textfield not working.
TextField("Units", value: $units, formatter: NumberFormatter())
.padding(10)
.background(Color("Settings"))
.cornerRadius(10)
.foregroundColor(Color("Background"))
.keyboardType(.numberPad)
Button (action: {
self.quantity = ((database.productList[selectFood].weight) * Double(self.units)) / 100
caloriesInput = database.productList[selectFood].calories * quantity
proteinInput = database.productList[selectFood].protein * quantity
carbsInput = database.productList[selectFood].carbs * quantity
fatsInput = database.productList[selectFood].fats * quantity
UIApplication.shared.hideKeyboard()
}) {
ZStack {
Rectangle()
.frame(width: 90, height: 40, alignment: .center)
.background(Color(.black))
.opacity(0.20)
.cornerRadius(15)
;
Text("Enter")
.foregroundColor(.white)
.fontWeight(.bold)
}
}
}
}
}
This is an issue with NumberFormatter that has been going on for a while. If you remove the formatter it updates correctly.
This is a workaround. Sadly it requires 2 variables.
import SwiftUI
struct TFConnection: View {
#State var unitsD: Double = 0
#State var unitsS = ""
var body: some View {
VStack{
//value does not get extracted properly
TextField("units", text: Binding<String>(
get: { unitsS },
set: {
if let value = NumberFormatter().number(from: $0) {
print("valid value")
self.unitsD = value.doubleValue
}else{
unitsS = $0
//Remove the invalid character it is not flawless the user can move to in-between the string
unitsS.removeLast()
print(unitsS)
}
}))
Button("enter"){
print("enter action")
print(unitsD.description)
}
}
}
}
struct TFConnection_Previews: PreviewProvider {
static var previews: some View {
TFConnection()
}
}

The compiler is unable to type-check this expression in a reasonable time in SwiftUI?

I have a line of code that sets the background of Text to an Image that is fetched by finding the first three letters of the string. For some reason this won't run and keeps giving me the error above. Any ideas on how I can fix this?
There are a lot of images that need to be set as the backgrounds for multiple different pieces of text. I believe I have the right idea by using the prefix of the string, but it seems like Xcode is having difficulty/won't run this.
Pretty sure this specific line is giving me issues, but would love some feedback.
.background(Image(colorOption.prefix(3)).resizable())
import SwiftUI
struct ColorView: View {
// #ObservedObject var survey = Survey()
#ObservedObject var api = ColorAPIRequest(survey: DataStore.instance.currentSurvey!)
#State var showingConfirmation = true
#State var showingColorView = false
#State var tempSelection = ""
#EnvironmentObject var survey: Survey
//#EnvironmentObject var api: APIRequest
var colorOptionsGrid: [[String]] {
var result: [[String]] = [[]]
let optionsPerRow = 4
api.colorOptions.dropFirst().forEach { colorOption in
if result.last!.count == optionsPerRow { result.append([]) }
result[result.count - 1].append(colorOption)
}
return result
}
var body: some View {
VStack {
Text("Select Tape Color")
.font(.system(size:70))
.bold()
.padding(.top, 20)
NavigationLink("", destination: LengthView(), isActive: $showingColorView)
HStack {
List {
ForEach(colorOptionsGrid, id: \.self) { colorOptionRow in
HStack {
ForEach(colorOptionRow, id: \.self) { colorOption in
Button(action: {
// self.survey.length = lengthOption
self.tempSelection = colorOption
self.showingConfirmation = false
}
) {
ZStack {
Color.clear
Text(colorOption.prefix(3))
.font(.title)
.foregroundColor(self.tempSelection == colorOption ? Color.white : Color.black)
.frame(width: 200, height: 100)
.background(Image(colorOption.prefix(3)).resizable())
//Image(colorOption.prefix(3)).resizable()
}
}.listRowBackground(self.tempSelection == colorOption ? Color.pink : Color.white)
.multilineTextAlignment(.center)
}
}
}
}.buttonStyle(PlainButtonStyle())
}
Button(action: {
self.survey.color = self.tempSelection
self.showingColorView = true
self.showingConfirmation = true
}) {
Text("Press to confirm \(tempSelection)")
.bold()
.padding(50)
.background(Color.pink)
.foregroundColor(.white)
.font(.system(size:40))
.cornerRadius(90)
}.isHidden(showingConfirmation)
.padding(.bottom, 50)
}
}
}
The compiler actually gives a fairly decent suggestion when it tells you to break the expression up. The simplest you can do is extract the background image into a separate function like this:
func backgroundImage(for colorOption: String) -> some View {
Image(String(colorOption.prefix(3))).resizable()
}
and then replace the call to
.background(Image(colorOption.prefix(3)).resizable())
with
.background(self.backgroundImage(for: colorOption))
Also note that I wrapped colorOption.prefix(3) in a String constructor, simply because .prefix(_:) returns a Substring, but the Image(_:) constructor requires a String.

Resources