SwiftUI get next item in ForEach loop - foreach

I'm trying to implement a VStack grid that needs to have a HStack when a condition is met on the next item in a ForEach loop. I have the code below that displays the items in an array but I don't know how to get the next item to check against.
Here's what I have so far.
VStack {
ForEach(item.cards, id: \.self) { item in
switch item.card.size {
case .Large:
LargeCard(card: item.card, viewModel: CardViewModel())
case .Medium:
MediumCard(card: item.card, viewModel: CardViewModel())
case .Small:
HStack(spacing: 16) {
SmallCard(card: item.card, viewModel: CardViewModel())
// This is where I think I need to check and I tried somthing like below, but the compiler crashes
if $0 + 1 < item.cards.count {
if item.card.size == .Small {
SmallCard(card: item.card, viewModel: CardViewModel())
}
}
}
case .none:
Text("No more.")
}
}
}
Here's the item struct:
struct Item: Decodable, Hashable {
let card: Card
}
Here's what I'm wanting to get.

you could try adding a id to your struct Item, such as:
struct Item: Identifiable, Decodable, Hashable {
let id = UUID()
let card: Card
}
and then use:
VStack {
ForEach(item.cards, id: \.self) { theItem in
switch theItem.card.size {
case .Large:
LargeCard(card: theItem.card, viewModel: CardViewModel())
case .Medium:
MediumCard(card: theItem.card, viewModel: CardViewModel())
case .Small:
HStack(spacing: 16) {
SmallCard(card: theItem.card, viewModel: CardViewModel())
// here use the id to find the next item
if let ndx = item.cards.firstIndex(where: {$0.id == theItem.id}) {
if ndx + 1 < item.cards.count {
let nextItem = item.cards[ndx + 1]
if nextItem.card.size == .Small {
SmallCard(card: nextItem.card, viewModel: CardViewModel())
}
}
}
}
case .none:
Text("No more.")
}
}
}
You could also use enumerated as mentioned in the comments, such as:
VStack {
ForEach(Array(item.cards.enumerated()), id: \.offset) { index, theItem in
switch theItem.card.size {
case .Large:
LargeCard(card: theItem.card, viewModel: CardViewModel())
case .Medium:
MediumCard(card: theItem.card, viewModel: CardViewModel())
case .Small:
HStack(spacing: 16) {
SmallCard(card: theItem.card, viewModel: CardViewModel())
// here use the index to find the next item
if index + 1 < item.cards.count {
let nextItem = item.cards[index + 1]
if nextItem.card.size == .Small {
SmallCard(card: nextItem.card, viewModel: CardViewModel())
}
}
}
case .none:
Text("No more.")
}
}
}
Note, it looks like you should be using .environmentObject(viewModel) to pass
a single CardViewModel() to the views, instead of creating a new CardViewModel() each time.

Related

How to change the selection of the second picker after changing the first picker?

Here I'm trying to create the first little app that helps to convert a value from one unit of measurement to another.
The first conversion picker helps me to select data for the second picker(from which measure I'm going to converse) and the third picker(to which measure I'm going converse).
But when I'm changing the first picker - it doesn't change the #State value of the second and third pickers. I tried different approaches but the result is the same. Could you please help me?
struct ContentView: View {
#State private var userInput = 0.0
#State var selectionOfTypeOfConversion: Conversions = .temperature
#State var selectionFromConversion: String = TemperatureConversions.celsius.rawValue
#State var selectionToConversion: String = TemperatureConversions.celsius.rawValue
#FocusState private var inputIsFocused: Bool
var output: Double {
//doing all counting
}
var body: some View {
VStack {
NavigationView {
Form {
Section {
Picker("Type of conversion", selection: $selectionOfTypeOfConversion) {
ForEach(Conversions.allCases, id: \.self) {
Text($0.rawValue)
}
}.onSubmit {
checkConvention()
}
TextField("", value: $userInput, format: .number)
.keyboardType(.decimalPad)
.focused($inputIsFocused)
Picker("From conversion", selection: $selectionFromConversion) {
switch selectionOfTypeOfConversion {
case Conversions.temperature:
ForEach(TemperatureConversions.allCases, id: \.self) {
Text($0.rawValue).tag($0.rawValue)
}
case Conversions.length:
ForEach(LengthConversions.allCases, id: \.self) {
Text($0.rawValue).tag($0.rawValue)
}
case Conversions.volume:
ForEach(VolumeConversions.allCases, id: \.self) {
Text($0.rawValue).tag($0.rawValue)
}
}
}
} header: {
Text("From")
}
Section {
Picker("To conversion", selection: $selectionToConversion) {
switch selectionOfTypeOfConversion {
case Conversions.temperature:
ForEach(TemperatureConversions.allCases, id: \.self) {
Text($0.rawValue).tag($0.rawValue)
}
case Conversions.length:
ForEach(LengthConversions.allCases, id: \.self) {
Text($0.rawValue).tag($0.rawValue)
}
case Conversions.volume:
ForEach(VolumeConversions.allCases, id: \.self) {
Text($0.rawValue).tag($0.rawValue)
}
}
}
Text(output, format: .number)
} header: {
Text("To")
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
inputIsFocused = false
}
}
}
}
}
}
private func checkConvention() {
if selectionOfTypeOfConversion == Conversions.temperature {
selectionFromConversion = TemperatureConversions.celsius.rawValue
selectionToConversion = TemperatureConversions.celsius.rawValue
} else if selectionOfTypeOfConversion == Conversions.length{
selectionFromConversion = LengthConversions.meters.rawValue
selectionToConversion = LengthConversions.meters.rawValue
} else if selectionOfTypeOfConversion == Conversions.volume{
selectionFromConversion = VolumeConversions.milliliters.rawValue
selectionToConversion = VolumeConversions.milliliters.rawValue
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
enum Conversions: String, CaseIterable {
case temperature = "temperature"
case length = "length"
case volume = "volume"
}
enum TemperatureConversions: String, CaseIterable {
var id: Self { self }
case celsius = "celsius"
case fahrenheit = "fahrenheit"
case kelvin = "kelvin"
}
enum LengthConversions: String, CaseIterable {
var id: Self { self }
case meters = "meters"
case feet = "feet"
case miles = "miles"
}
enum VolumeConversions: String, CaseIterable {
var id: Self { self }
case milliliters = "ml"
case pints = "pints"
case gallons = "gallons"
}
Expecting: change the formula which counts input into output.

ForEach with NavigationLink(iOS 15)

I'm trying to do NavigationLink with ForEach Loop to different views, but i can't do it.
Only what i know, that i should to change something in this code, but what and how to do it...
I tried to put RestaurantsView(i created it), but it gives me error 'cuz i don't know where to put it more properly.
Right now it gives me an empty page, but this is not a view.
import Foundation
enum SideMenuText: Int, CaseIterable {
case restaurants
case coffeeShops
case groceryStores
case clothingStores
case electronicStores
case carDealers
var title: String {
switch self {
case .restaurants: return "Restaurants"
case .coffeeShops: return "Coffee Shops"
case .groceryStores: return "Grocery Stores"
case .clothingStores: return "Clothing Stores"
case .electronicStores: return "Electronic Stores"
case .carDealers: return "Car Dealers"
}
}
var imageName: String {
switch self {
case .restaurants: return "fork.knife"
case .coffeeShops: return "cup.and.saucer"
case .groceryStores: return "cart"
case .clothingStores: return "tshirt"
case .electronicStores: return "iphone"
case .carDealers: return "car"
}
}
}
And here:
import SwiftUI
struct SideMenuView: View {
#Binding var isShowing: Bool
var body: some View {
ZStack {
LinearGradient(gradient: Gradient(colors: [Color.blue, Color.black]), startPoint: .leading, endPoint: .bottom)
.ignoresSafeArea()
VStack {
SideMenuHeaderView(isShowing: $isShowing)
ForEach(SideMenuText.allCases, id: \.self) { option in
NavigationLink ( //
destination: Text(option.title),
label: { SideMenuOptions(viewModel: option)
})
}
Spacer()
}
}
.navigationBarHidden(true) //
}
}
struct SideMenuView_Previews: PreviewProvider {
static var previews: some View {
SideMenuView(isShowing: .constant(true))
}
}

SwiftUI - How to use enum switch to View other Screens

I am trying to great a side menu navigation to so the user is able to select each category within the menu and it will display the screen that I build to the user. For Example in the use selects "Fire Number" it will show the CalcFireNum file I created to the user. I have built a Enum with switch statements and cant figure out how to get the screens to show instead of the Strings.
import Foundation
import SwiftUI
enum SideMenuViewModel: Hashable {
case profile
case Mainscreen
case FireNumber
case Mindset
case Debt
case help
case logout
var title: String {
switch self {
case .profile: return "Profile"
case .Mainscreen: return "Main Screen"
case .FireNumber: return "Calculate Fire Number"
case .Mindset: return "Wants Vs Needs"
case .Debt: return "Debt Reduction"
case .help: return "Help"
case .logout: return "Logout"
}
}
var imageName: String {
switch self {
case .profile: return "Profile"
case .Mainscreen: return "Lists"
case .FireNumber: return "Bookmarks"
case .Mindset: return "Wants Vs Needs"
case .Debt: return "Debt Reduction"
case .help: return "help"
case .logout: return "Logout"
}
}
}
I am also running the a ForEach loop for the SideMenuViewModel
import SwiftUI
struct SideMenu: View {
#Binding var ishowing: Bool
var body: some View {
ZStack{
LinearGradient(gradient: Gradient(colors: [Color.white, Color.brown]),
startPoint: .top, endPoint: .bottom)
.ignoresSafeArea()
VStack {
SideMenuHeaderView(isShowing: $ishowing)
.frame(height: 240)
ForEach(SideMenuViewModel.allCases, id: \.self) { option in
NavigationLink(destination: Text(option.title), label: {
SideMenuOptionView(viewModel: option)
})
}
Spacer()
}
}.navigationBarHidden(true)
}
And also have a SideMenuOptionView File that is for the users profile photo thats being called from the enum in the sideMenuViewModel
import SwiftUI
struct SideMenuOptionView: View {
let viewModel: SideMenuViewModel
var body: some View {
HStack(spacing: 16) {
Image(systemName: viewModel.imageName)
.frame(width: 24, height: 24)
Text(viewModel.title)
.font(.system(size: 15, weight: .semibold))
Spacer()
}
.foregroundColor(.white)
.padding()
}
}
struct SideMenuOptionView_Previews: PreviewProvider {
static var previews: some View {
SideMenuOptionView(viewModel: .help)
}
}

Can a Swiftui Identifiable Foreach loop select only the first 5 items

Howdy I have a project that only needs to display the first 5 items in the loop of over 30 items, below is my code
struct Introductions: Codable, Identifiable {
let id: String
let topIntros: String?
let image: String
let date: String
}
ForEach(introductions) { introduction in
NavigationLink(destination: IntroductionDetailView(introduction: introduction)) {
IntroductionsView(introduction: introduction)
}
}
I tried using this method but xcode crashed crashed after i scrolled past the fifth item
ForEach(introductions, id: \.topIntros) { introduction in
NavigationLink(destination: IntroductionDetailView(introduction: introduction)) {
IntroductionsView(introduction: introduction)
}
}
Thanks
you could try something like this:
if (introductions.count >= 5) {
ForEach(introductions.prefix(upTo: 5), id:\.id) { introduction in
NavigationLink(destination: IntroductionDetailView(introduction: introduction)) {
IntroductionsView(introduction: introduction)
}
}
}
Here is a simple way for you:
struct ContentView: View {
#State private var array: [Int] = Array(100...130)
var body: some View {
if (array.count >= 5) {
ForEach(0...4, id:\.self) { index in
Text(String(describing: array[index]))
}
}
}
}
Here another way for you:
struct ContentView: View {
#State private var array: [Int] = Array(100...130)
var body: some View {
ForEach(array.dropLast(array.count - 5), id:\.self) { item in
Text(String(describing: item))
}
}
}
Using #workingdog 's comment i was able to fix it and posting the answer below in case it comes in handy.
struct Introductions: Codable, Identifiable {
let id: String
let topIntros: String?
let image: String
let date: String
}
ForEach(introductions) { introduction in
NavigationLink(destination: IntroductionDetailView(introduction: introduction)) {
IntroductionsView(introduction: introduction)
}
}
struct ContentView: View {
var body: some View {
VStack {
if (introductions.count >= 5) {
ForEach(introductions.prefix(upTo: 5), id:\.topIntros) { introduction in
NavigationLink(destination: IntroductionDetailView(introduction: introduction)) {
IntroductionsView(introduction: introduction)
}
}
} else {
ForEach(introductions) { introduction in
NavigationLink(destination: IntroductionDetailView(introduction: introduction)) {
IntroductionsView(introduction: introduction)
}
}
}
}
}
}
using .id broke the view hierarchy so i used the optional .topIntros and voila every turned out fine.

SwiftUI multiple vertical lists row animations not working

Suddenly I have figured out that there is no automatic animation for non of them:
no animations for inserting, removing, reordering, etc. ☹️ Ids are unique and persistent between transitions
Question 1
How can I make them work as expected?
Question 2
Is there a simple way to have an animation for seeing the transition between lists? Like moving a row between sections of a single list.
Explaining:
Let's say I have three lists that group different states of elements of a single array:
extension Call {
enum State: Equatable {
case inProgress
case accepted
case rejected
}
}
The observable:
class CallManager: ObservableObject {
#Published var calls: [Call]
init(visits: [Call] = []) { self.calls = visits }
}
And Call is a simple Identifiable:
struct Call: Identifiable & Equatable & Hashable {
let id = UUID()
var state: State
}
By making these bindings, I have bind all lists to the core calls array:
extension CallManager {
func bindingCalls(for state: Call.State) -> Binding<[Call]> {
Binding<[Call]>(
get: { self.calls.filter { $0.state == state } },
set: { // TODO: Find a better way for this
self.calls.removeAll(where: { $0.state == state })
self.calls.append(contentsOf: $0)
}
)
}
var inProgress: Binding<[Call]> { bindingCalls(for: .inProgress) }
var accepted: Binding<[Call]> { bindingCalls(for: .accepted) }
var rejected: Binding<[Call]> { bindingCalls(for: .rejected) }
}
And here is the View code:
struct ContentView: View {
#StateObject var visitManager = CallManager(visits: [
Call(state: .inProgress),
Call(state: .accepted),
Call(state: .accepted),
Call(state: .inProgress),
Call(state: .inProgress),
Call(state: .rejected)
])
var body: some View {
HStack {
List(visitManager.inProgress) { $call in
CallView(call: $call)
}
List(visitManager.accepted) { $call in
CallView(call: $call)
}
List(visitManager.rejected) { $call in
CallView(call: $call)
}
}
}
}
struct CallView: View & Identifiable {
#Binding var call: Call
var id: UUID { call.id }
var body: some View {
Text(call.id.uuidString.prefix(15))
.foregroundColor(call.state.color)
.onTapGesture(count: 2) { call.state = .rejected }
.onTapGesture { call.state = .accepted }
}
}
extension Call.State {
var color: Color {
switch self {
case .inProgress: return .blue
case .rejected: return .red
case .accepted: return .green
}
}
}
You can enable the animations on the List view:
List(visitManager.inProgress) { $call in
CallView(call: $call)
}
.animation(.default)
Or wrap the changes in a withAnimation block:
.onTapGesture { withAnimation { call.state = .accepted } }
As for the animation between the columns, you can get something like that with .matchedGeometryEffect. afaik it will always look a bit crumbly between List, to make it look good you need to use a VStack (but then loose all the comfort of the List view). For example:
import SwiftUI
extension Call {
enum State: Equatable, CaseIterable {
case inProgress
case accepted
case rejected
}
}
class CallManager: ObservableObject {
#Published var calls: [Call]
init(visits: [Call] = []) { calls = visits }
}
struct Call: Identifiable & Equatable & Hashable {
let id = UUID()
var state: State
}
struct ContentView: View {
#Namespace var items
#StateObject var visitManager = CallManager(visits: [
Call(state: .inProgress),
Call(state: .accepted),
Call(state: .accepted),
Call(state: .inProgress),
Call(state: .inProgress),
Call(state: .rejected),
])
var body: some View {
HStack(alignment: .top) {
ForEach(Call.State.allCases, id: \.self) { state in
VStack {
List(visitManager.calls.filter { $0.state == state }) { call in
CallView(call: call)
.id(call.id)
.matchedGeometryEffect(id: call.id, in: items)
.onTapGesture {
if let idx = visitManager.calls.firstIndex(where: { $0.id == call.id }) {
withAnimation {
visitManager.calls[idx].state = .rejected
}
}
}
}
}
}
}
}
}
struct CallView: View & Identifiable {
var call: Call
var id: UUID { call.id }
var body: some View {
Text(call.id.uuidString.prefix(15))
.foregroundColor(call.state.color)
}
}
extension Call.State {
var color: Color {
switch self {
case .inProgress: return .blue
case .rejected: return .red
case .accepted: return .green
}
}
}

Resources