#Published Bool never updates to true - ios

I am working on a SwiftUI App. Below is my code for an observalbleObject view model
class ProductViewModel: ObservableObject {
#EnvironmentObject var viewModel: LoginViewModel
#Published var productList = ProductApiData(results: [])
#Published var success = false
func getProducts(_ params: String...) {
ProductApi().getProducts(sessionId: params[0], status: params[1], searchText: params[2], completion: { result, data in
if result == .success {
guard let data = data else { return }
let decodedResponse = try! JSONDecoder().decode(ProductApiData.self, from: data as! Data)
DispatchQueue.main.sync {
self.productList = decodedResponse
}
}
})
}
func updateProduct (_ params: String...) {
ProductApi().updateProduct(sessionId: params[0], parentProductAsin: params[1], productTitle: params[2], parentCategory: params[3], productCateogry: params[4], productTeam: params[5], productSubCategory: params[6], carouselProduct: params[7], featuredProduct: params[8], isHot: params[9], isNew: params[10], numReviews: params[11], rating: params[12], productPrice: params[13], productImagePath: params[14], completion: { result, data in
if result == .success {
guard let data = data else { return }
let decodedResponse = try! JSONDecoder().decode(ApiCallData.self, from: data as! Data)
DispatchQueue.main.sync {
if decodedResponse.success{
self.success = true
print("!")
}
}
}
})
}
}
Below is the code for my view
import Foundation
import SwiftUI
struct ProductInformationView: View {
#EnvironmentObject var quantityViewModel: QuantityViewModel
#EnvironmentObject var orderItemViewModel: OrderItemViewModel
#EnvironmentObject var viewModel: LoginViewModel
#EnvironmentObject var productViewModel: ProductViewModel
#EnvironmentObject var orderItemViewModlel: OrderItemViewModel
#EnvironmentObject var orderViewModel: OrderViewModel
#StateObject var localNotification = LocalNotification()
#State var product: Product
#State private var initalQuantity = "0"
#State private var datePickerSelection = Date()
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}()
var body: some View {
Form {
Group {
HStack {
Text("Parent Asin")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", text: $quantityViewModel.quantityList.results[0].parentProductAsin).multilineTextAlignment(.trailing)
}
}
HStack {
Text("Product Asin")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", text: $quantityViewModel.quantityList.results[0].productAsin).multilineTextAlignment(.trailing)
}
}
TextEditor(text: $product.productTitle)
Picker(selection: $product.parentCategory, label: Text("Parent Category")) {
ForEach(["Maryland", "Terps", "MLB", "NFL"], id: \.self) {
Text($0).tag($0)
}
}
Picker(selection: $product.productTeam, label: Text("Product Team")) {
ForEach(["Maryland", "Orioles", "Yankees", "Tampa Bay", "NA"], id: \.self) {
Text($0).tag($0)
}
}
Picker(selection: $product.productCategory, label: Text("Product Category")) {
ForEach(["Clothing", "HeadWear", "Footwear", "Socks", "Accessories", "Automobile","ShotGlass","Decals and Magnets"], id: \.self) {
Text($0).tag($0)
}
}
Picker(selection: $product.productSubCategory, label: Text("Product Sub Category")) {
ForEach(["Kids", "Mens", "Women", "Unisex", "Youth", "Toddler", "Infant", "License Plate","Decal"], id: \.self) {
Text($0).tag($0)
}
}
HStack {
Text("Number of Reviews")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", value: $product.numReviews, formatter: NumberFormatter()).multilineTextAlignment(.trailing)
}
}
HStack {
Text("Rating")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", value: $product.rating, formatter: NumberFormatter()).multilineTextAlignment(.trailing)
}
}
}
Group {
Toggle(isOn: $product.isHotProduct) {
Text("Is Hot")
}
Toggle(isOn: $product.featuredProduct) {
Text("Is Featured")
}
Toggle(isOn: $product.carouselProduct) {
Text("Is Carousel")
}
Toggle(isOn: $product.isNewProduct) {
Text("Is New")
}
HStack {
Text("Box Number")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", text: $quantityViewModel.quantityList.results[0].productBoxNumber).multilineTextAlignment(.trailing)
}
}
HStack {
Text("Product Image Path")
Spacer()
if (product.productHasVariations == true) {
} else {
TextField("", text: $product.productImagePath).multilineTextAlignment(.trailing)
}
/*if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", text: .bindOptional($quantityViewModel.quantityList.results[0].productImagePath, "")).multilineTextAlignment(.trailing)
}*/
}
HStack {
Text("Product Price")
Spacer()
if (product.productHasVariations == true) {
} else {
TextField("", value: $product.productPrice, formatter: formatter).multilineTextAlignment(.trailing)
}
/*if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", text: .bindOptional($quantityViewModel.quantityList.results[0].productPrice, "") ).multilineTextAlignment(.trailing)
}*/
}
HStack {
Text("Total Quantity")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", value: $quantityViewModel.quantityList.results[0].totalQuantity, formatter: NumberFormatter()).multilineTextAlignment(.trailing)
}
}
HStack {
Text("Sold Quantity")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", value: $quantityViewModel.quantityList.results[0].soldQuantity, formatter: NumberFormatter()).multilineTextAlignment(.trailing)
}
}
HStack {
Text("In-Stock Quantity")
Spacer()
if ($quantityViewModel.quantityList.results.count == 0) {
TextField("", text: $initalQuantity).multilineTextAlignment(.trailing)
} else {
TextField("", value: $quantityViewModel.quantityList.results[0].inStockQuantity, formatter: NumberFormatter()).multilineTextAlignment(.trailing)
}
}
}
Button("Update") {
updateProductInformation {
}
/*updateQuantityInformation()
updateOrderItemInformation()
updateOrderInformation()
localNotification.setLocalNotification(title: product.productTitle, subtitle: product.productAsin, body: "asd", when: 5)
*/ }
}.onAppear(perform: setQuantityInformation)
}
func setQuantityInformation() {
self.quantityViewModel.getQuantities(self.viewModel.user.token!, "PARENT", product.productAsin)
}
func updateProductInformation(finished: () ->Void ) {
print("#1")
self.productViewModel.updateProduct(self.viewModel.user.token!,
$product.productAsin.wrappedValue,
$product.productTitle.wrappedValue,
$product.parentCategory.wrappedValue,
$product.productCategory.wrappedValue,
$product.productTeam.wrappedValue,
$product.productSubCategory.wrappedValue,
String($product.carouselProduct.wrappedValue),
String($product.featuredProduct.wrappedValue),
String($product.isHotProduct.wrappedValue),
String($product.isNewProduct.wrappedValue),
String($product.numReviews.wrappedValue),
String($product.rating.wrappedValue),
String($product.productPrice.wrappedValue),
$product.productImagePath.wrappedValue)
print($productViewModel.success.wrappedValue)
if($productViewModel.success.wrappedValue) {
print("#2")
updateQuantityInformation()
print("#3")
}
print($productViewModel.success.wrappedValue)
}
func updateQuantityInformation() {
self.quantityViewModel.updateQuantities(self.viewModel.user.token!,
$quantityViewModel.quantityList.results[0].productAsin.wrappedValue,
String($quantityViewModel.quantityList.results[0].totalQuantity.wrappedValue),
String($quantityViewModel.quantityList.results[0].inStockQuantity.wrappedValue),
$quantityViewModel.quantityList.results[0].productBoxNumber.wrappedValue,
"", "", "", $product.productTitle.wrappedValue)
}
func updateOrderItemInformation() {
self.orderItemViewModel.updateOrderItem(self.viewModel.user.token!,
$quantityViewModel.quantityList.results[0].productAsin.wrappedValue,
$quantityViewModel.quantityList.results[0].productBoxNumber.wrappedValue,
$product.productImagePath.wrappedValue,
String($product.productPrice.wrappedValue))
}
func updateOrderInformation() {
self.orderViewModel.updateOrder(sessionId: self.viewModel.user.token!,
orderItems: $orderItemViewModel.orderItemsListIds.wrappedValue)
}
}
Clicking the update button on my view calls a function updateProductInformation which in return calls an API to update the information. As part of the above function I have
if($productViewModel.success.wrappedValue) {
print("#2")
updateQuantityInformation()
print("#3")
}
Everytime I run this the observedvariable is always false but i am sure I am updating it as part of the observable object. Any idea why is this happening

Related

Issue with Binding passed to TextField, it is not allowing state change

Hi all thanks in advance.
I am having an issue when running app (not in preview), a textfield is not updating the state. I've not continued to expand the MVVM yet as I am getting caught up in this UI/Binding issue.
Not sure what have I missed here? I am passing a StateObject (view model instance) into the EnvironmentObject list, which is then accessed from an EnvironmentObject and the models array of elements in a view is iterated over, then further passing the iterated elements of the array to a Binding in another view which is then bound to a textfield to be edited by the user?
Specifically, the issue is:
When swipe action > edit on an expense in the ContentView to navigate to EditExpenseView, the textfields don't allow editing.
Note:
If I move the textfield up to the ExpenseList View, the binding to edit works. I thought that maybe the List(items) was the issue because it's iterating over an immutable collection.
I am using the index and passing the array binding via $expenses[index] which is avoiding accessing the immutable collection as its only being used to get the index of the list item the user will edit.
If your still reading, thanks for being awesome!
Let me know if I can add any further information or provide clarity.
Expense Model:
struct Expense: Equatable, Identifiable, Codable {
init(date: Date, description: String, amount: Decimal, type: ExpenseType, status: ExpenseStatus, budgetId: UUID?) {
self.date = date
self.description = description
self.amount = amount
self.type = type
self.status = status
self.budgetId = budgetId
}
static func == (lhs: Expense, rhs: Expense) -> Bool {
lhs.id == rhs.id
}
var id: UUID = UUID()
var date: Date
var description: String
var amount: Decimal
var type: ExpenseType
var status: ExpenseStatus
var budgetId: UUID?
}
ExpenseViewModel:
class ExpenseViewModel: ObservableObject, Identifiable {
#Published var expenses: [Expense] = []
func insertExpense(date: Date, description: String, amount: Decimal, type: ExpenseType, status: ExpenseStatus) -> Void {
expenses.insert(Expense(date: date, description: description, amount: amount, type: type, status: status, budgetId: nil), at:0)
}
func remove(_ expense: Expense) {
expenses.removeAll(where: {$0.id == expense.id})
}
}
App Entry:
import SwiftUI
#main
struct iBudgeteerApp: App {
#StateObject private var expenses = ExpenseViewModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(expenses)
}
}
}
Initial View:
struct ContentView: View {
#EnvironmentObject private var model: ExpenseViewModel
private static let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
var body: some View {
NavigationStack {
VStack {
Button("Add Row") {
model.insertExpense(date: Date(), description: "Groceries", amount: 29.94, type: .Expense, status: .Cleared)
}
ExpenseList(expenses: $model.expenses)
}
}
}
}
Expense List View:
struct ExpenseList: View {
#Binding var expenses: [Expense]
var formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
var body: some View {
List (expenses.sorted(by: {$0.date > $1.date}).indices, id: \.self) {
index in
HStack {
Text("\(index + 1).").padding(.trailing)
VStack(alignment: .leading) {
HStack {
Text(expenses[index].date.formatted(date:.numeric, time: .omitted))
Spacer()
Text(expenses[index].description)
}
HStack {
Text(expenses[index].description)
Spacer()
Text("\(expenses[index].amount as NSNumber, formatter: formatter)")
.foregroundColor( expenses[index].type == .Expense ? .red : .green)
Image(systemName: expenses[index].type == .Expense ? "arrow.down" : "arrow.up").foregroundColor( expenses[index].type == .Expense ? .red : .green)
}.padding(.top, 1)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive, action: { expenses.remove(at: index) } ) {
Label("Delete", systemImage: "trash")
}
.tint(.gray)
}
.swipeActions() {
NavigationLink {
EditExpenseView(expense: self.$expenses[index])
} label: {
Label("Edit", systemImage: "slider.horizontal.3")
}
.tint(.yellow)
}
}
}
}
}
Edit Expense View:
struct EditExpenseView: View {
#Binding var expense: Expense
var formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
var body: some View {
Form {
Section(header: Text("Editing: \(expense.description)")) {
VStack {
DatePicker(
"Date",
selection: $expense.date,
displayedComponents: [.date]
)
HStack {
Text("Name")
Spacer()
TextField("description",text: $expense.description)
.fixedSize().multilineTextAlignment(.trailing)
}
HStack {
Text("Amount")
Spacer()
TextField("0.00", value: $expense.amount, formatter: formatter).fixedSize()
}
Picker("Status", selection: $expense.status) {
ForEach(ExpenseStatus.allCases, id: \.self) {
status in
Text("\(status.rawValue)")
}
}
Picker("Type", selection: $expense.type) {
ForEach(ExpenseType.allCases, id: \.self) {
type in
Text("\(type.rawValue)")
}
}
}
}
}
}
}
UPDATE
It works in:
List ($expenses) { $expense in
NavigationLink(expense.description) {
EditExpenseView(expense: $expense)
}
}
ForEach($expenses) { $expense in
NavigationLink(expense.description) {
EditExpenseView(expense: $expense)
}
}
But not in:
List($expenses) {
$expense in
VStack(alignment: .leading) {
HStack {
Text(expense.date.formatted(date:.numeric, time: .omitted))
Spacer() }
HStack {
Text(expense.description)
Spacer()
Text("\(expense.amount as NSNumber, formatter: formatter)")
.foregroundColor( expense.type == .Expense ? .red : .green)
Image(systemName: expense.type == .Expense ? "arrow.down" : "arrow.up").foregroundColor(expense.type == .Expense ? .red : .green)
}.padding(.top, 1)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive, action: { //expenses.remove(expense)
} ) {
Label("Delete", systemImage: "trash")
}
.tint(.gray)
}
.swipeActions() {
NavigationLink {
EditExpenseView(expense: $expense)
} label: {
Label("Edit", systemImage: "slider.horizontal.3")
}
.tint(.yellow)
}
}
Disclaimer:
I couldnĀ“t test this answer properly as your example is missing information and is not reproducible. Please consider posting a minimal reproducible example.
The issue is in these lines:
List (expenses.sorted(by: {$0.date > $1.date}).indices, id: \.self) {
and then doing:
EditExpenseView(expense: self.$expenses[index])
You are not passing a binding reference of Expense on to your EditExpenseView but a binding to a copy of it. You are breaking the binding chain.
The following aproach should yield the desired result:
List ($expenses) { $expense in
HStack {
Text("\(expenses.firstIndex(of: expense) + 1).").padding(.trailing)
VStack(alignment: .leading) {
HStack {
Text(expense.date.formatted(date:.numeric, time: .omitted))
Spacer()
Text(expense.description)
}
.....
and passing your Expense on to your subview:
EditExpenseView(expense: $expense)

SwiftUI cannot update CoreData Entity Object

I am facing a very weird bug, I have a edit function which will accept the object and new form data from the frontend, the function can be executed without errors but the issue is the data is not updated at all. I tried reopen the simulator and refetch also no use to this. May I ask how to solve this?
class RoomDataController: ObservableObject {
#Published var rooms: [Room] = []
let container = NSPersistentContainer(name: "RentalAppContainer")
init() {
container.loadPersistentStores { description, error in
let current = FileManager.default.currentDirectoryPath
print(current)
if let error = error {
print("Failed to load data in DataController \(error.localizedDescription)")
}
}
fetchRooms()
}
func save(context: NSManagedObjectContext) {
do {
try context.save()
print("Data created or updated successfully!")
} catch {
// Handle errors in our database
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
func editRoom(room: Room, roomType: String, roomDescription: String, contactPeriod: String, roomPrice: Float, userID: UUID, isShort: Bool, isLong: Bool, images: String, email: String, phoneNum:String, context: NSManagedObjectContext) {
room.roomType = roomType
room.roomPrice = roomPrice
room.roomDescription = roomDescription
room.contactPeriod = contactPeriod
room.contactEmail = email
room.contactNum = phoneNum
room.updated_at = Date()
print("room to be edited: ", room)
save(context: container.viewContext)
fetchRooms()
}
}
The Frontend View that call the edit function
struct EditRoomView: View {
#EnvironmentObject var userAuth: UserAuth
#State var type: String = ""
#State var description: String = ""
#State var price: String = ""
#State var contactPeriod: String = ""
#State var isShort: Bool = false
#State var isLong: Bool = false
#State var email: String = ""
#State var phoneNum: String = ""
#ObservedObject var room: Room
private func editItem() {
RoomDataController().editRoom(
room: room,
roomType: self.type,
roomDescription: self.description,
contactPeriod: self.contactPeriod,
roomPrice: Float(self.price)!,
userID: userAuth.user.id!,
isShort: self.isShort,
isLong: self.isLong,
images: "room-1,room-2,room-3",
email: email,
phoneNum: phoneNum
)
}
init(room: Room){
self.room = room
_type = State(initialValue: room.roomType!)
_description = State(initialValue: room.roomDescription!)
_price = State(initialValue: String( room.roomPrice))
_contactPeriod = State(initialValue: room.contactPeriod!)
_email = State(initialValue: room.contactEmail!)
_phoneNum = State(initialValue: room.contactNum!)
if room.contactPeriod == "Short"{
_isShort = State(initialValue:true)
_isLong = State(initialValue: false)
} else {
_isShort = State(initialValue: false)
_isLong = State(initialValue:true)
}
}
var body: some View {
VStack{
HStack {
Spacer()
VStack {
Menu {
Button {
// self.selectedTab = .AccountViewTab
} label: {
NavigationLink(destination: AccountView().environmentObject(userAuth), label: {Text("My Profile")})
Image(systemName: "arrow.down.right.circle")
}
Button {
userAuth.isLoggedin = false
} label: {
Text("Log Out")
Image(systemName: "arrow.up.and.down.circle")
}
} label: {
Text("Menu").foregroundColor(Color.black)
}
}
Spacer()
VStack {
NavigationLink(destination: OwnerNavbarView().environmentObject(userAuth), label: {Text("Room Rent").foregroundColor(Color.black)})
}
.onTapGesture {
}
Spacer()
VStack {
NavigationLink(destination: OwnerNavbarView(nextView: "PostRoomHistoryViewTab").environmentObject(userAuth), label: {Text("Post History").foregroundColor(Color.black)})
}
.onTapGesture {
}
Spacer()
VStack {
NavigationLink(destination: OwnerNavbarView(nextView: "CustomerOrderViewTab").environmentObject(userAuth), label: {Text("Customer Orders").foregroundColor(Color.black)})
}
.onTapGesture {
}
Spacer()
}
.padding(.bottom)
.background(Color.gray.edgesIgnoringSafeArea(.all))
Text("Edit Room Details")
Form {
Section(header: Text("Room Type") ) {
TextField("", text: self.$type)
}
Section(header: Text("Room Description") ) {
TextField("", text: self.$description)
}
MultipleImagePickerView()
Section(header: Text("Room Price") ) {
TextField("", text: self.$price)
}
Section(header: Text("Contact Period") ) {
HStack{
Toggle( "Short", isOn: $isShort )
Toggle("Long", isOn: $isLong )
}
}
Section(header: Text("Contact Email Address * :") ) {
TextField("", text: self.$email)
}
Section(header: Text("Contact Mobile No * :") ) {
TextField("", text: self.$phoneNum)
}
}
Button(action: {
self.editItem()
}) {
HStack {
Text("Update")
}
.padding()
.scaledToFit()
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(5)
}
}
}
}
Ok..Managed to solve this in this way, buy why?
do {
fetchRequest.predicate = NSPredicate(format: "id == %#", room.id?.uuidString as! CVarArg)
let result = try container.viewContext.fetch(fetchRequest)
result[0].roomType = roomType
result[0].roomPrice = roomPrice
result[0].roomDescription = roomDescription
result[0].contactPeriod = contactPeriod
result[0].contactEmail = email
result[0].contactNum = phoneNum
result[0].updated_at = Date()
} catch {
print ("fetch task failed", error)
}
if room.ownerID == userID.uuidString {
save(context: container.viewContext)
fetchRooms()
}

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)
}
}

SwiftUI - Present 3 different Views with different parameter

I need to present 3 different Views.
AddListView
ChangeColor
EditListView
They take different paramater. AddListView does not have parameter while ChangeColor and EditListView takes Color and NSManagedObject respectively. However for the sake of simplicity, EditListView's paramter is integer in this example.
I am using .fullScreenCover(item: <#T##Binding<Identifiable?>#>, content: <#T##(Identifiable) -> View#>) for presenting them.
.fullScreenCover(item: $presentedViewType) { type in
if type == .AddListView {
AddListView()
}
else if type == .EditListView {
if let index = selectedIndex {
EditListView(index: index)
}
}
else if type == .ChangeColor {
if let color = selectedColor {
ColorView(color: color)
}
}
}
selectedIndex and selectedColor is nil even though I initialize them before initializing presentedViewType. And hence, an EmptyView is presented.
This is the project.
enum PresentedViewType: Identifiable {
case AddListView
case ChangeColor
case EditListView
var id: Int {
return hashValue
}
}
struct ContentView: View {
#State var presentedViewType: PresentedViewType?
#State var selectedColor: Color?
#State var selectedIndex: Int?
var body: some View {
NavigationView {
List {
Section {
NavigationLink(destination: Text("All")) {
Text("All")
}
.background(Color.blue)
.contextMenu {
Button(action: {
selectedColor = .blue
presentedViewType = .ChangeColor
}) {
Label("Change Color", systemImage: "paintbrush.pointed.fill")
}
}
}
ForEach(0..<10) { index in
NavigationLink(destination: Text("Row Details \(index)")) {
Text("Row \(index)")
}
.contextMenu {
Button(action: {
selectedIndex = index
presentedViewType = .EditListView
}) {
Label("Edit", systemImage: "square.and.pencil")
}
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
presentedViewType = .AddListView
}) {
Label("Add", systemImage: "plus")
}
}
}
.fullScreenCover(item: $presentedViewType) { type in
if type == .AddListView {
AddListView()
}
else if type == .EditListView {
if let index = selectedIndex {
EditListView(index: index)
}
}
else if type == .ChangeColor {
if let color = selectedColor {
ColorView(color: color)
}
}
}
}
}
}
struct ColorView: View {
#Environment(\.presentationMode) var presentationMode
#State var color: Color
var body: some View {
NavigationView {
Text("Color View")
.background(color)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
}
}
}
}
}
}
}
struct AddListView: View {
#Environment(\.presentationMode) var presentationMode
#State var text: String = ""
var body: some View {
NavigationView {
TextField("", text: $text)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
}
}
}
}
}
}
}
struct EditListView: View {
#Environment(\.presentationMode) var presentationMode
#State var index: Int
var body: some View {
NavigationView {
Text("Row \(index)")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
}
}
}
}
}
}
}
I have to mention that they do not have fixed value. They have different value depending on which row you need to edit.
How to pass selectedIndex and selectedColor to EditListView and ColorView respectively?
Update
EditListView takes only selectedIndex while ColorView takes only selectedColor
You need to have #Binding properties inside EditListView and ColorView
struct EditListView: View {
#Binding var selectedIndex: Int?
// rest of view implementation
}
struct ColorView: View {
#Binding var selectedIndex: Int?
// rest of view implementation
}
and then pass the binding in the initialisers
.fullScreenCover(item: $presentedViewType) { type in
if type == .AddListView {
AddListView()
} else if type == .EditListView {
EditListView(index: $selectedIndex)
} else if type == .ChangeColor {
ColorView(color: $selectedColor)
}
}

How do I create a transition between from Login View to Tab View in SwiftUI?

Here's the snippet of code from LoginView:
Button(action: {
if loginAndPasswordAreOK() {
// Perform Segue to TabView
} else {
self.isValidLoginAndPassword = false
self.email = ""
self.password = ""
}
}, label: {
and there's a piece of code of MainTabView (aka Home Tab):
struct MainTabView: View {
var body: some View {
TabView {
Text("Home Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
I googled around and saw NavigationLink or something but I don't want to wrap up this transition to a NavController at all.
Here:
import SwiftUI
struct ContentView: View {
#State var isPassOk: Bool = false
var userPass: String = "1234"
#State var userGivenPass: String = ""
var body: some View {
if isPassOk == false
{
HStack
{
TextField("Enter your Pass Here!", text: $userGivenPass)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Log in") {
// do your logig Here!
if userGivenPass == userPass
{
withAnimation(.easeInOut)
{
isPassOk = true
}
}
else
{
isPassOk = false
}
}
}
.font(Font.title)
.padding()
}
else if isPassOk == true
{
TabView {
Text("Home Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
}
}
}
}
Here Updated for you:
import SwiftUI
struct ContentView: View {
#State var isPassOk: Bool = false
var body: some View {
if isPassOk == false
{
LogInView(isPassOk: $isPassOk)
}
else if isPassOk == true
{
MainTabView()
}
}
}
struct LogInView: View {
#Binding var isPassOk: Bool
var userPass: String = "1234"
#State var userGivenPass: String = ""
var body: some View {
HStack
{
TextField("Enter your Pass Here!", text: $userGivenPass)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Log in") {
// do your logig Here!
if userGivenPass == userPass
{
withAnimation(.easeInOut)
{
isPassOk = true
}
}
else
{
isPassOk = false
}
}
}
.font(Font.title)
.padding()
}
}
struct MainTabView: View {
var body: some View {
TabView {
Text("Home Tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
}
}
}

Resources