SwiftUI - List that does not display - ios

I don't know how to explain it, I think a video will be more explicit ..
https://www.youtube.com/watch?v=UyuGaNA6NCo
I only want to display the information according to my choices, and I don't know where I was wrong, I am in the problem since some hours
My code :
HOME VIEW
struct HomeView: View {
#ObservedObject var travelLibrary: TravelLibrary
#State private var isShowingSurveyCreation = false
var body: some View {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) {
NavigationView {
ScrollView {
VStack {
ForEach(travelLibrary.testTravels) { travel in
ZStack {
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color.white)
.shadow(color: /*#START_MENU_TOKEN#*/.black/*#END_MENU_TOKEN#*/, radius: 7)
TravelCellView(travel: travel)
}
}
}.padding(12)
}.navigationTitle(Text("Mes Voyages"))
}
PlusButtonView(action: {
isShowingSurveyCreation.toggle()
}).sheet(isPresented: $isShowingSurveyCreation, content: {
TravelCreationView(travelLibrary: travelLibrary)
})
.padding()
}
}
}
struct HomeView_Previews: PreviewProvider {
#StateObject static var travelLibrary = TravelLibrary()
static var previews: some View {
HomeView(travelLibrary: travelLibrary)
}
}
TRAVEL CREATION VIEW
import SwiftUI
struct TravelCreationView: View {
#ObservedObject var travelLibrary: TravelLibrary
#ObservedObject var travelConfig = NewTravelConfig()
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
ZStack {
Form {
Section {
Picker(selection: $travelConfig.index1, label: Text("Pays de destination : ")) {
ForEach(0 ..< travelConfig.nameCountry.count) {
Text(travelConfig.nameCountry[$0]).tag($0)
.foregroundColor(.black)
}
}
Picker(selection: $travelConfig.index2, label: Text("Type de voyage : ")) {
ForEach(0 ..< travelConfig.typeOfTravel.count) {
Text(travelConfig.typeOfTravel[$0]).tag($0)
.foregroundColor(.black)
}
}
}
Section {
Stepper(value: $travelConfig.day, in: 1...365) {
Text("Durée du séjour : \(travelConfig.day) jours")
}
}
}
}.navigationTitle("Formulaire")
.navigationBarItems(trailing: Button(action: {
let newTravel = Travel(config: travelConfig)
travelLibrary.testTravels.append(newTravel)
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Valider")
}))
}
}
}
struct TravelCreationView_Previews: PreviewProvider {
#StateObject static var travelLibrary = TravelLibrary()
static var previews: some View {
TravelCreationView(travelLibrary: travelLibrary)
}
}
//
// TravelCreationView.swift
// Final Travel Project Logistics
//
// Created by Sefkan on 04/05/2021.
//
import SwiftUI
struct TravelCreationView: View {
#ObservedObject var travelLibrary: TravelLibrary
#ObservedObject var travelConfig = NewTravelConfig()
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
ZStack {
Form {
Section {
Picker(selection: $travelConfig.index1, label: Text("Pays de destination : ")) {
ForEach(0 ..< travelConfig.nameCountry.count) {
Text(travelConfig.nameCountry[$0]).tag($0)
.foregroundColor(.black)
}
}
Picker(selection: $travelConfig.index2, label: Text("Type de voyage : ")) {
ForEach(0 ..< travelConfig.typeOfTravel.count) {
Text(travelConfig.typeOfTravel[$0]).tag($0)
.foregroundColor(.black)
}
}
}
Section {
Stepper(value: $travelConfig.day, in: 1...365) {
Text("Durée du séjour : \(travelConfig.day) jours")
}
}
}
}.navigationTitle("Formulaire")
.navigationBarItems(trailing: Button(action: {
let newTravel = Travel(config: travelConfig)
travelLibrary.testTravels.append(newTravel)
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Valider")
}))
}
}
}
struct TravelCreationView_Previews: PreviewProvider {
#StateObject static var travelLibrary = TravelLibrary()
static var previews: some View {
TravelCreationView(travelLibrary: travelLibrary)
}
}//
// TravelCreationView.swift
// Final Travel Project Logistics
//
// Created by Sefkan on 04/05/2021.
//
import SwiftUI
struct TravelCreationView: View {
#ObservedObject var travelLibrary: TravelLibrary
#ObservedObject var travelConfig = NewTravelConfig()
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
ZStack {
Form {
Section {
Picker(selection: $travelConfig.index1, label: Text("Pays de destination : ")) {
ForEach(0 ..< travelConfig.nameCountry.count) {
Text(travelConfig.nameCountry[$0]).tag($0)
.foregroundColor(.black)
}
}
Picker(selection: $travelConfig.index2, label: Text("Type de voyage : ")) {
ForEach(0 ..< travelConfig.typeOfTravel.count) {
Text(travelConfig.typeOfTravel[$0]).tag($0)
.foregroundColor(.black)
}
}
}
Section {
Stepper(value: $travelConfig.day, in: 1...365) {
Text("Durée du séjour : \(travelConfig.day) jours")
}
}
}
}.navigationTitle("Formulaire")
.navigationBarItems(trailing: Button(action: {
let newTravel = Travel(config: travelConfig)
travelLibrary.testTravels.append(newTravel)
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Valider")
}))
}
}
}
struct TravelCreationView_Previews: PreviewProvider {
#StateObject static var travelLibrary = TravelLibrary()
static var previews: some View {
TravelCreationView(travelLibrary: travelLibrary)
}
}
TRAVELCELLVIEW
//
// TravelCellView.swift
// Final Travel Project Logistics
//
// Created by Sefkan on 04/05/2021.
//
import SwiftUI
struct TravelCellView: View {
let travel : Travel
var body: some View {
HStack {
Image(travel.flagCountry)
.resizable()
.aspectRatio(contentMode: /*#START_MENU_TOKEN#*/.fill/*#END_MENU_TOKEN#*/)
.frame(width: 80, height: 80)
.clipShape(/*#START_MENU_TOKEN#*/Circle()/*#END_MENU_TOKEN#*/)
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
.padding(.trailing, 8)
VStack(alignment : .leading) {
Text(travel.nameCountry)
.font(.title3)
.fontWeight(.bold)
.foregroundColor(Color.black)
.padding(.bottom, -4)
Text("Durée du voyage: \(travel.day) jours")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(Color.black)
.padding(.bottom, -4)
Text(travel.typeOfTravel)
.font(.subheadline)
.foregroundColor(Color.black)
}
Spacer()
FavouriteButtonView()
}.padding()
}
}
struct TravelCellView_Previews: PreviewProvider {
private static let testTravel = Travel(flagCountry: "USAflag", nameCountry: "États-Unis", typeOfTravel: "Tourisme", day: 0, isFavourite: false)
static var previews: some View {
TravelCellView(travel: testTravel)
.previewLayout(.sizeThatFits)
}
}
DATA VIEW
import Foundation
struct Travel: Identifiable {
let id = UUID().uuidString
let flagCountry: String
let nameCountry: String
let typeOfTravel: String
let day: Int
var isFavourite: Bool
init(flagCountry: String, nameCountry: String, typeOfTravel: String, day: Int, isFavourite: Bool) {
self.flagCountry = flagCountry
self.nameCountry = nameCountry
self.typeOfTravel = typeOfTravel
self.day = day
self.isFavourite = isFavourite
}
init(config: NewTravelConfig) {
flagCountry = ""
nameCountry = ""
typeOfTravel = ""
day = 0
isFavourite = false
}
}
class TravelLibrary: ObservableObject {
#Published var testTravels = [
Travel(flagCountry: "USAflag", nameCountry: "États-Unis", typeOfTravel: "Tourisme", day: 0, isFavourite: false),
Travel(flagCountry: "Japanflag", nameCountry: "Japon", typeOfTravel: "Tourisme", day: 0, isFavourite: true),
Travel(flagCountry: "Germanflag", nameCountry: "Allemagne", typeOfTravel: "Tourisme", day: 0, isFavourite: false)
]
}
class NewTravelConfig: ObservableObject {
#Published var nameCountry = ["États-Unis", "Japon", "Allemagne"]
#Published var typeOfTravel = ["Tourisme", "Business"]
#Published var day = 0
#Published var index1 = 0
#Published var index2 = 0
}
Thanks!

Your issue is inside the Travel struct. You have two init() methods, and the issue is in the one where you pass your config.
You are passing the config to it, but not using any of these values from the config.
Here is the correct one:
init(config: NewTravelConfig) {
flagCountry = ""
nameCountry = config.nameCountry[config.index1]
typeOfTravel = config.typeOfTravel[config.index2]
day = config.day
isFavourite = false
}

Related

Swiftui items get duplicated in all views when added to a single custom view

I'm struggling with the following issue: I'm trying to build a very simple app that lets you add items in a dedicated view that can be collapsed. I managed to write a simple function that lets me add multiple of these custom collapsable views. It's my first app so I wanted to follow the MVVM protocol. I think I got confused along the way because now every item I add gets automatically added to all the custom collapsable views I made. Is there any way to fix this? I thought using the UUID would solve this issue.. I'm guessing that I have to customise the "saveButtonPressed" function, but I don't know how to tell it to only add the item to the view where I pressed the "plus" button..
Here are the Models for the individual items and the collapsable view:
struct ItemModel: Identifiable, Equatable {
let id: String
let title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel {
return ItemModel(id: id, title: title)
}
}
--
import Foundation
struct CollapsableItem: Equatable, Identifiable, Hashable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> CollapsableItem {
return CollapsableItem(id: id, title: title)
}
}
These are my two ViewModels:
class ListViewModel: ObservableObject {
#Published var items: [ItemModel] = []
init() {
getItems()
}
func getItems() {
let newItems = [
ItemModel(title: "List Item1"),
ItemModel(title: "List Item2"),
ItemModel(title: "List Item3"),
]
items.append(contentsOf: newItems)
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
--
class CollapsedViewModel: ObservableObject {
#Published var collapsableItems: [CollapsableItem] = []
#Published var id = UUID().uuidString
init() {
getCollapsableItems()
}
func getCollapsableItems() {
let newCollapsableItems = [
CollapsableItem(title: "Wake up")
]
collapsableItems.append(contentsOf: newCollapsableItems)
}
func addCollapsableItem(title: String) {
let newCollapsableItem = CollapsableItem(title: title)
collapsableItems.append(newCollapsableItem)
}
func updateCollapsableItem(collapsableItem: CollapsableItem) {
if let index = collapsableItems.firstIndex(where: { $0.id ==
collapsableItem.id}) {
collapsableItems[index] =
collapsableItem.updateCompletion()
}
}
}
The item view:
struct ListRowView: View {
#EnvironmentObject var listViewModel: ListViewModel
let item: ItemModel
var body: some View {
HStack() {
Text(item.title)
.font(.body)
.fontWeight(.bold)
.foregroundColor(.white)
.multilineTextAlignment(.center)
.lineLimit(1)
.frame(width: 232, height: 16)
}
.padding( )
.frame(width: 396, height: 56)
.background(.gray)
.cornerRadius(12.0)
}
}
The collapsable view:
struct CollapsedView2<Content: View>: View {
#State var collapsableItem: CollapsableItem
#EnvironmentObject var collapsedViewModel: CollapsedViewModel
#State private var collapsed: Bool = true
#EnvironmentObject var listViewModel: ListViewModel
#State var label: () -> Text
#State var content: () -> Content
#State private var show = true
var body: some View {
ZStack{
VStack {
HStack{
Button(
action: { self.collapsed.toggle() },
label: {
HStack() {
Text("Hello")
.font(.title2.bold())
Spacer()
Image(systemName: self.collapsed ? "chevron.down" :
"chevron.up")
}
.padding(.bottom, 1)
.background(Color.white.opacity(0.01))
}
)
.buttonStyle(PlainButtonStyle())
Button(action: saveButtonPressed, label: {
Image(systemName: "plus")
.font(.title2)
.foregroundColor(.white)
})
}
VStack {
self.content()
}
ForEach(listViewModel.items) { item in ListRowView (item: item)
}
.lineLimit(1)
.fixedSize(horizontal: true, vertical: true)
.frame(minWidth: 396, maxWidth: 396, minHeight: 0, maxHeight: collapsed ?
0 : .none)
.animation(.easeInOut(duration: 1.0), value: show)
.clipped()
.transition(.slide)
}
}
}
func saveButtonPressed() {
listViewModel.addItem(title: "Hello")
}
}
and finally the main view:
struct ListView: View {
#EnvironmentObject var listViewModel: ListViewModel
#EnvironmentObject var collapsedViewModel: CollapsedViewModel
var body: some View {
ZStack{
ScrollView{
VStack{
HStack{
Text("MyFirstApp")
.font(.largeTitle.bold())
Button(action: newCollapsablePressed, label: {
Image(systemName: "plus")
.font(.title2)
.foregroundColor(.white)
})
}
.padding()
.padding(.leading)
ForEach(collapsedViewModel.collapsableItems) { collapsableItem in
CollapsedView2 (collapsableItem: collapsableItem,
label: { Text("") .font(.title2.bold()) },
content: {
HStack {
Text("")
Spacer() }
.frame(maxWidth: .infinity)
})
}
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.statusBar(hidden: false)
.navigationBarHidden(true)
}
}
func newCollapsablePressed() {
collapsedViewModel.addCollapsableItem(title: "hello2")
}
}
Would love to understand how I could fix this!
There is the anwser for your comment about add item in each CollapsedView2.
Because ListViewModel is not ObservableObject (ListViewModel is difference from each CollapsableItem). You should use "#State var items: [ItemModel]".
struct CollapsedView2<Content: View>: View {
#State var collapsableItem: CollapsableItem
// #State var listViewModel = ListViewModel()
#State var collapsed: Bool = true
#State var label: () -> Text
#State var content: () -> Content
#State private var show = true
#State var items: [ItemModel] = []
#State var count = 1
var body: some View {
VStack {
HStack{
Text("Hello")
.font(.title2.bold())
Spacer()
Button( action: { self.collapsed.toggle() },
label: {
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
)
.buttonStyle(PlainButtonStyle())
Button(action: saveButtonPressed, label: {
Image(systemName: "plus")
.font(.title2)
// .foregroundColor(.white)
})
}
VStack {
self.content()
}
ForEach(items) { item in
ListRowView (item: item)
}
.lineLimit(1)
.fixedSize(horizontal: true, vertical: true)
.frame(minHeight: 0, maxHeight: collapsed ? 0 : .none)
.animation(.easeInOut(duration: 1.0), value: show)
.clipped()
.transition(.slide)
}
}
func saveButtonPressed() {
addItem(title: "Hello \(count)")
count += 1
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
There is the anwser. Ask me if you have some questions
struct ListView: View {
#StateObject var collapsedViewModel = CollapsedViewModel()
var body: some View {
ScrollView{
VStack{
HStack{
Text("MyFirstApp")
.font(.largeTitle.bold())
Button(action: newCollapsablePressed, label: {
Image(systemName: "plus")
.font(.title2)
// .foregroundColor(.white)
})
}
ForEach(collapsedViewModel.collapsableItems) { collapsableItem in
CollapsedView2 (collapsableItem: collapsableItem,
label: { Text("") .font(.title2.bold()) },
content: {
HStack {
Text("")
Spacer()
}
})
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.statusBar(hidden: false)
.navigationBarHidden(true)
}
func newCollapsablePressed() {
collapsedViewModel.addCollapsableItem(title: "hello2")
}
}
struct CollapsedView2<Content: View>: View {
#State var collapsableItem: CollapsableItem
#State var listViewModel = ListViewModel()
#State var collapsed: Bool = true
#State var label: () -> Text
#State var content: () -> Content
#State private var show = true
var body: some View {
VStack {
HStack{
Button( action: { self.collapsed.toggle() },
label: {
HStack() {
Text("Hello")
.font(.title2.bold())
Spacer()
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
.padding(.bottom, 1)
.background(Color.white.opacity(0.01))
}
)
.buttonStyle(PlainButtonStyle())
Button(action: saveButtonPressed, label: {
Image(systemName: "plus")
.font(.title2)
.foregroundColor(.white)
})
}
VStack {
self.content()
}
ForEach(listViewModel.items) { item in
ListRowView (item: item)
}
.lineLimit(1)
.fixedSize(horizontal: true, vertical: true)
.frame(minHeight: 0, maxHeight: collapsed ? 0 : .none)
.animation(.easeInOut(duration: 1.0), value: show)
.clipped()
.transition(.slide)
}
}
func saveButtonPressed() {
listViewModel.addItem(title: "Hello")
}
}
struct ListRowView: View {
let item: ItemModel
var body: some View {
HStack() {
Text(item.title)
.font(.body)
.fontWeight(.bold)
.foregroundColor(.white)
.multilineTextAlignment(.center)
.lineLimit(1)
.frame(width: 232, height: 16)
}
.padding( )
.frame(width: 396, height: 56)
.background(.gray)
.cornerRadius(12.0)
}
}
class ListViewModel {
var items: [ItemModel] = []
init() {
getItems()
}
func getItems() {
let newItems = [
ItemModel(title: "List Item1"),
ItemModel(title: "List Item2"),
ItemModel(title: "List Item3"),
]
items.append(contentsOf: newItems)
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
class CollapsedViewModel: ObservableObject {
#Published var collapsableItems: [CollapsableItem] = []
#Published var id = UUID().uuidString
init() {
getCollapsableItems()
}
func getCollapsableItems() {
let newCollapsableItems = [
CollapsableItem(title: "Wake up")
]
collapsableItems.append(contentsOf: newCollapsableItems)
}
func addCollapsableItem(title: String) {
let newCollapsableItem = CollapsableItem(title: title)
collapsableItems.append(newCollapsableItem)
}
func updateCollapsableItem(collapsableItem: CollapsableItem) {
if let index = collapsableItems.firstIndex(where: { $0.id ==
collapsableItem.id}) {
collapsableItems[index] =
collapsableItem.updateCompletion()
}
}
}
struct CollapsableItem: Equatable, Identifiable, Hashable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> CollapsableItem {
return CollapsableItem(id: id, title: title)
}
}
struct ItemModel: Identifiable, Equatable {
let id: String
let title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel {
return ItemModel(id: id, title: title)
}
}

using SwiftUI, when scrolling, VStack inside Section shrinks and expands

I'm trying to make simple testing app with Buttons and Options for Button action.
target is iOS 13.0 for backward compatibility.
expected smooth scroll just like other iOS app does, but it stutters.
some VStack is continuously shrink and expand, keep changing scrolls height so it stutters I think.
here's video: https://imgur.com/a/QguC4g0
if I stop just right at stutter-bottom point, there's shrunken VStack.
you can see that shrunken VStack at end of video.
here's the code of main View.
(shrunken Section is SectionH.)
import SwiftUI
struct ContentView: View {
#Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
#Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
var body: some View {
if(verticalSizeClass == .regular && horizontalSizeClass == .compact) {
VStack {
List {
SectionA()
SectionB()
SectionC()
SectionD()
SectionE()
SectionF()
SectionG()
SectionH()
}
OtherView()
}
} else {
HStack {
List {
SectionA()
SectionB()
SectionC()
SectionD()
SectionE()
SectionF()
SectionG()
SectionH()
}
OtherView()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and heres simple examples of other Sections(A to G are similar.)
import SwiftUI
struct SectionF: View {
#State var bool1 : Bool = true;
#State var text1: String = "text"
#State var text2: String = "text"
#State var text3: String = "text"
#State var text4: String = "text"
var body: some View {
Section(header: Text("Section F")) {
Button{
} label: {
Text("button")
}
VStack(alignment: .leading) {
Button{
} label: {
Text("button: \(bool1 ? "true" : "false")")
}
Toggle("bool", isOn: $bool1)
}
VStack(alignment: .leading) {
Button{
} label: {
Text("button")
}
HStack {
Text("text: ")
TextField("text", text: $text1)
}
HStack {
Text("text: ")
TextField("text", text: $text2)
}
HStack {
Text("text: ")
TextField("text", text: $text3)
}
HStack {
Text("text: ")
TextField("text", text: $text4)
}
}
VStack(alignment: .leading) {
Button{
} label: {
Text("button")
}
HStack {
Text("text: ")
TextField("text", text: $text1)
}
}
}
}
}
struct SectionF_Previews: PreviewProvider {
static var previews: some View {
Form {
SectionF()
}
}
}
and here's dreaded Section H.
import SwiftUI
struct SectionH: View {
#State var text1: String = "text"
#State var text2: String = "text"
#State var picker1: String = "1"
var picker1s = ["1", "2"]
#State var text3: String = "text"
#State var text4: String = "text"
#State var bool1 : Bool = true;
#State var text5: String = "text"
#State var bool2 : Bool = true;
#State var bool3 : Bool = true;
#State var number1: String = "0"
var body: some View {
Section(header: Text("Section H")) {
VStack(alignment: .leading) {
Button{
} label: {
Text("button")
}
HStack {
Text("text: ")
TextField("text", text: $text1)
}
HStack {
Text("text: ")
TextField("text", text: $text2)
}
VStack(alignment: .leading) {
Text("picker: \(picker1)")
Picker("picker", selection: $picker1) {
ForEach(picker1s, id: \.self) {
Text($0)
}
}.pickerStyle(.segmented)
}
HStack {
Text("text: ")
TextField("text", text: $text3)
}
HStack {
Text("text: ")
TextField("text", text: $text4)
}
}
Button {
} label: {
Text("Button")
}
VStack(alignment: .leading) {
Button {
} label: {
Text("Button")
}
VStack {
HStack {
Text("text: ")
TextField("text", text: $text1)
}
HStack {
Text("text: ")
TextField("text", text: $text2)
}
VStack(alignment: .leading) {
Text("picker: \(picker1)")
Picker("picker", selection: $picker1) {
ForEach(picker1s, id: \.self) {
Text($0)
}
}.pickerStyle(.segmented)
}
}
//**this is shrinking VStack!**
VStack {
Toggle("bool", isOn: $bool1)
HStack {
Text("text: ")
TextField("text", text: $text3)
}
HStack {
Text("text: ")
TextField("text", text: $text5)
}
Toggle("bool", isOn: $bool2)
Toggle("bool", isOn: $bool3)
HStack {
Text("number: ")
TextField("number", text: $number1)
}
}
}
}
}
}
struct SectionH_Previews: PreviewProvider {
static var previews: some View {
Form {
SectionH()
}
}
}
here's what I tried:
changing List to Form
changing Sections order
removing V/HStack changing by orientation <- this one removes stutter!
reducing options from SectionH
divide SectionHs options into 2 VStack <- is applied to code above.
I'm new to Swift and SwiftUI, I don't know why this happens and can't get any clues to solve this problem.
thanks in advice.
UPDATE
removed SectionH, not fixed.
removed OtherView, not fixed.
I think theres a clue on if state with orientation.

Updating EnvironmentObject in child view causes view to pop

I have a child view that updates an EnvironmentObject that then causes the child view to pop back to its parent view. I am creating an app that uses similar "Like" functionality from this tutorial: https://www.hackingwithswift.com/books/ios-swiftui/letting-the-user-mark-favorites
Every time the like button is clicked and the EnvironmentObject likes object is updated, the view pops to the previous (ProductGridView) view instead of staying on the child view (ProductDetailView).
struct ContentView: View {
#State private var tabSelection = 0
#ObservedObject var products = Products()
#ObservedObject var favorites = Favorites()
var body: some View {
VStack {
TabView(selection: $tabSelection) {
NavigationView{
ProductGridView()
}
.tabItem { Image(systemName: "megaphone")
Text("Products")
}.tag(0)
.environmentObject(products)
.environmentObject(favorites)
}
struct ProductGridView: View {
var columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
#EnvironmentObject var products: Products
#EnvironmentObject var favorites: Favorites
var body: some View {
VStack{
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: columns, alignment: .leading, spacing: 20) {
ForEach(products.products, content: {
product in
NavigationLink(destination: ProductDetailView(product: product)) {
ProductCellView(product: product)
.padding(.horizontal, 10)
}
})
}
}.onAppear() {
self.products.fetchData()
}
}
struct ProductDetailView: View {
let product: Product
#EnvironmentObject var favorites: Favorites
var body: some View {
ScrollView(showsIndicators: false) {
VStack{
ProductImageView(product: product)
Button(action: {
if favorites.contains(product) {
favorites.remove(product) //Updating here causes issue
} else {
favorites.add(product) //Updating here causes issue
}
}) {
if favorites.contains(product){
Image(systemName: "heart.fill")
}
else{
Image(systemName: "heart")
}
}
}
class Favorite : Identifiable, Encodable {
var id = UUID()
var name: String
...
class Favorites: ObservableObject {
#Published private var products: [String]?
...
struct Product: Identifiable{
let id = UUID()
let productname: String
...
It looks like your fetch data call may be causing the issue.
struct YourContentView: View {
#State private var tabSelection = 0
#ObservedObject var products = Products()
#ObservedObject var favorites = Favorites()
var body: some View {
VStack {
TabView(selection: $tabSelection) {
NavigationView{
ProductGridView()
}
.tabItem { Image(systemName: "megaphone")
Text("Products")
}.tag(0)
.environmentObject(products)
.environmentObject(favorites)
}
}
}
}
struct ProductDetailView: View {
let product: Product
#EnvironmentObject var favorites: Favorites
var body: some View {
ScrollView(showsIndicators: false) {
VStack{
Image(systemName: "photo")
.resizable()
.scaledToFit()
Button {
if favorites.products?.contains(product) ?? false {
favorites.remove(product) //Updating here causes issue
} else {
favorites.add(product) //Updating here causes issue
}
} label: {
if favorites.products?.contains(product) ?? false{
Image(systemName: "heart.fill")
}
else{
Image(systemName: "heart")
}
}
}
}
}
}
struct ProductCellView: View {
#State var product: Product
var body: some View {
HStack {
Image(systemName: "photo")
Text(product.productname)
}
}
}
struct ProductGridView: View {
let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
#EnvironmentObject var products: Products
#EnvironmentObject var favorites: Favorites
var body: some View {
VStack{
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: columns, alignment: .leading, spacing: 20) {
ForEach(products.products, content: {
product in
NavigationLink(destination: ProductDetailView(product: product)) {
ProductCellView(product: product)
.padding(.horizontal, 10)
}
})
}
}.onAppear() {
self.products.fetchData()
}
}
}
}
class Favorites: ObservableObject {
#Published var products: [Product]? = []
init() {}
func remove(_ product: Product) {
products?.removeAll(where: { $0.id == product.id })
}
func add(_ product: Product) {
products?.append(product)
}
}
class Products: ObservableObject, Identifiable {
#Published var products: [Product]
init() {
products = [.init(productname: "ApplePie"), .init( productname: "Cheeseburger")]
}
func fetchData() {
// uncommenting this code will cause product grid view to reload because it relies on products
// products = [.init(productname: "ApplePie"), .init( productname: "Cheeseburger")]
}
}
struct Product: Identifiable, Equatable {
let id = UUID()
let productname: String
public static func ==(lhs: Product, rhs: Product) -> Bool {
lhs.id == rhs.id
}
}

Swift - Update List from different View

I have 2 Views in my Swift Project and when I click on the Button on the secondView, I want to update the List in the First View. I don't know how to do it! If I use a static variable in my MainView and then edit this variable from the secondView, it works, but it won't update. And if I don't use static and instead use #State, it would update, but I can't access it from my secondView.
Here is the Code below:
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
MainView()
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("MainView")
}
}.tag(0)
UpdateOtherViewFromHere()
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("SecondView")
}
}.tag(1)
}
}
}
struct MainView: View {
var arrayList: [CreateListItems] = []
init() {
let a = CreateListItems(name: "First Name!")
let b = CreateListItems(name: "Second Name!")
let c = CreateListItems(name: "Third Name!")
arrayList.append(a)
arrayList.append(b)
arrayList.append(c)
}
var body: some View {
return VStack {
ZStack {
NavigationView {
List {
ForEach(arrayList) { x in
Text("\(x.name)")
}
}.navigationBarTitle("Main View")
}
}
}
}
}
struct UpdateOtherViewFromHere: View {
func updateList() {
//Code that should remove "FirstName" from the List in MainView
}
var body: some View {
return VStack {
Button(action: {
updateList()
}) {
Image(systemName: "heart.slash")
.font(.largeTitle)
Text("Click Me!")
}
}
}
}
struct CreateListItems: Identifiable {
var id: UUID = UUID()
var name: String
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can share it using #State and #Binding if you put
struct ContentView: View {
#State var arrayList: [CreateListItems] = []
struct MainView: View {
#Binding var arrayList: [CreateListItems]
struct UpdateOtherViewFromHere: View {
#Binding var arrayList: [CreateListItems]
or you use the MVVM pattern and store the list in an ObservableObject and use #StateObject/#ObservedObject (source) and use #EnvironmentObject(connection) to share it between your Views.
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
class ParentViewModel: ObservableObject{
#Published var arrayList: [CreateListItems] = []
init(){
addSamples()
}
func addSamples() {
let a = CreateListItems(name: "First Name!")
let b = CreateListItems(name: "Second Name!")
let c = CreateListItems(name: "Third Name!")
arrayList.append(a)
arrayList.append(b)
arrayList.append(c)
}
func updateList() {
let a = CreateListItems(name: "\(arrayList.count + 1) Name!")
arrayList.append(a)
}
}
struct ParentView: View {
#StateObject var vm: ParentViewModel = ParentViewModel()
var body: some View {
TabView {
MainView().environmentObject(vm)
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("MainView")
}
}.tag(0)
UpdateOtherViewFromHere().environmentObject(vm)
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("SecondView")
}
}.tag(1)
}
}
}
struct MainView: View {
#EnvironmentObject var vm: ParentViewModel
var body: some View {
return VStack {
ZStack {
NavigationView {
List {
ForEach(vm.arrayList) { x in
Text(x.name)
}
}.navigationBarTitle("Main View")
}
}
}
}
}
struct UpdateOtherViewFromHere: View {
#EnvironmentObject var vm: ParentViewModel
var body: some View {
return VStack {
Button(action: {
vm.updateList()
}) {
Image(systemName: "heart.slash")
.font(.largeTitle)
Text("Click Me!")
}
}
}
}

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.

Resources