SwiftUI manipulate items from a struct from a view - ios

I'd like the ability to edit and put into a new view the 'expenses' the user adds. I've been having problems accessing the data after a new expense has been added. I am able to delete the items and add them up but I'd like to click on the 'expenses' and see and edit the content in them Image of the view
//Content View
import SwiftUI
struct ExpenseItem: Identifiable, Codable {
let id = UUID()
let name: String
let type: String
let amount: Int
}
class Expenses: ObservableObject {
#Published var items = [ExpenseItem]() {
didSet {
let encoder = JSONEncoder()
if let encoded = try?
encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "Items")
}
}
}
init() {
if let items = UserDefaults.standard.data(forKey: "Items") {
let decoder = JSONDecoder()
if let decoded = try?
decoder.decode([ExpenseItem].self, from: items) {
self.items = decoded
return
}
}
}
// Computed property that calculates the total amount
var total: Int {
self.items.reduce(0) { result, item -> Int in
result + item.amount
}
}
}
struct ContentView: View {
#ObservedObject var expenses = Expenses()
#State private var showingAddExpense = false
var body: some View {
NavigationView {
List {
ForEach(expenses.items) { item in
HStack {
VStack {
Text(item.name)
.font(.headline)
Text(item.type)
}
Spacer()
Text("$\(item.amount)")
}
}
.onDelete(perform: removeItems)
// View that shows the total amount of the expenses
HStack {
Text("Total")
Spacer()
Text("\(expenses.total)")
}
}
.navigationBarTitle("iExpense")
.navigationBarItems(trailing: Button(action: {
self.showingAddExpense = true
}) {
Image(systemName: "plus")
}
)
.sheet(isPresented: $showingAddExpense) {
AddView(expenses: self.expenses)
}
}
}
func removeItems(at offsets: IndexSet) {
expenses.items.remove(atOffsets: offsets)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
//AddExpense
import SwiftUI
struct AddView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var expenses: Expenses
#State private var name = ""
#State private var type = "Personal"
#State private var amount = ""
static let types = ["Business", "Personal"]
var body: some View {
NavigationView {
Form {
TextField("Name", text: $name)
Picker("Type", selection: $type) {
ForEach(Self.types, id: \.self) {
Text($0)
}
}
TextField("Amount", text: $amount)
.keyboardType(.numberPad)
}
.navigationBarTitle("Add new expense")
.navigationBarItems(trailing: Button("Save") {
if let actualAmount = Int(self.amount) {
let item = ExpenseItem(name: self.name, type: self.type, amount: actualAmount)
self.expenses.items.append(item)
self.presentationMode
.wrappedValue.dismiss()
}
})
}
}
}
struct AddView_Previews: PreviewProvider {
static var previews: some View {
AddView(expenses: Expenses())
}
}

Remove #observedObject in AddView.
A view cannot change an ObservableObject. ObservableObject is used for being notified when a value is changed.
When you pass the expenses class to AddView, you are giving it a reference. Therefore, AddView can change the expenses, and consequently update ContentView.

Related

How to setup NavigationLink inside SwiftUI list

I am attempting to set up a SwiftUI weather app. when the user searches for a city name in the textfield then taps the search button, a NavigationLink list item should appear in the list. Then, the user should be able to click the navigation link and re-direct to a detail view. My goal is to have the searched navigation links to populate a list. However, my search cities are not populating in the list, and I'm not sure why. In ContentView, I setup a list with a ForEach function that passes in cityNameList, which is an instance of the WeatherViewModel. My expectation is that Text(city.title) should display as a NavigationLink list item. How should I configure the ContentView or ViewModel to populate the the list with NavigationLink list items? See My code below:
ContentView
import SwiftUI
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
List {
ForEach(viewModel.cityWeather, id: \.id) { city in
NavigationLink(destination: DetailView(detail: viewModel)) {
HStack {
Text(city.cityWeather.name)
.font(.system(size: 32))
}
}
}
}
Spacer()
}
.navigationTitle("Weather MVVM")
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel
import Foundation
class WeatherViewModel: ObservableObject {
//everytime these properties are updated, any view holding onto an instance of this viewModel will go ahead and updated the respective UI
#Published var cityWeather: WeatherModel = WeatherModel()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=<MyAPIKey>") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, _, error in
// get data
guard let data = data, error == nil else {
return
}
//convert data to model
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityWeather = model
}
}
catch {
print(error)
}
}
task.resume()
}
}
Model
import Foundation
struct WeatherModel: Identifiable, Codable {
var id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
DetailView
import SwiftUI
struct DetailView: View {
var detail: WeatherViewModel
var body: some View {
VStack(spacing: 20) {
Text(detail.cityWeather.name)
.font(.system(size: 32))
Text("\(detail.cityWeather.main.temp)")
.font(.system(size: 44))
Text(detail.cityWeather.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherViewModel.init())
}
}
try something like this example code, works well for me:
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
List {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city)) {
HStack {
Text(city.name).font(.system(size: 32))
}
}
}
}
Spacer()
}.navigationTitle("Weather MVVM")
}.navigationViewStyle(.stack)
}
}
struct DetailView: View {
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name).font(.system(size: 32))
Text("\(detail.main.temp)").font(.system(size: 44))
Text(detail.firstWeatherInfo()).font(.system(size: 24))
}
}
}
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=YOURKEY") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}

SwiftUI iOS 14 View does not Update #Published Array with #EnvironmentObject

I'm working on a calorie-tracker app.
In my App, I can open the Detail side of some products, set the amount and add the product to the "Cart".
Later, I want to read out all collected datas from the array and show them an a short overview.
But this View won't be updated after making changer on the array.
Due to I storage the datas with the userDefaults, I always have to reopen the app to update the view. Only then, the hole array will be displayed.
My Class Cart:
import Foundation
struct Order: Equatable, Identifiable, Codable {
var id = UUID()
var product: Product
var modifier: Double
var date: Date
}
class Cart: ObservableObject {
#Published var orders = [Order]()
static let saveKey = "SavedData"
init() {
if let data = UserDefaults.standard.data(forKey: Self.saveKey) {
if let decoded = try? JSONDecoder().decode([Order].self, from: data) {
self.orders = decoded
}
} else {
self.orders = []
}
}
// save order
func save() {
if let encoded = try? JSONEncoder().encode(self.orders) {
UserDefaults.standard.set(encoded, forKey: Self.saveKey)
}
}
// add to order
func add(order: Order) {
self.orders.append(order)
print("product added to cart")
save()
}
// remove from order
func remove(order: Order) {
if let index = orders.firstIndex(of: order) {
orders.remove(at: index)
}
}
}
I made a View to apply the amount of any special product.
import SwiftUI
struct AmountView: View {
#EnvironmentObject var cart: Cart
#State private var textInput = ""
#State private var orderFinished = false
var product: Product
func StringDoubleConverter(text: String) -> String {
return String(format: "%.2f", Double(textInput.replacingOccurrences(of: ",", with: ".")) ?? 0)
}
var body: some View {
VStack {
Form {
Section(header: Text("Mengenangabe")) {
// input for the amount
AmountInputView(textInput: $textInput)
if !orderFinished {
Button("Hinzufügen", action: {
orderFinished = true
hideKeyboard()
// add product to the cart
self.cart.add(order: Order(product: product, modifier: Double(StringDoubleConverter(text: textInput))!, date: Date()))
})
.disabled(textInput == "")
.animation(.default)
} else {
Text("Wurde zum Logbuch hinzugefügt")
.foregroundColor(.blue)
}
}
productNutritionCollectionView(product: product, modifier: Double(StringDoubleConverter(text: textInput))!)
}
}
}
}
struct AmountView_Previews: PreviewProvider {
static var previews: some View {
AmountView(product: Product.exampleProduct).environmentObject(Cart())
}
}
Then, I want to display all products in the order in a logbook view using a Form and a ForEach lope.
struct LogbookView: View {
func deleteProducts(at offsets: IndexSet) {
cart.orders.remove(atOffsets: offsets)
cart.save()
}
#EnvironmentObject var cart: Cart
#State private var date = Date()
var body: some View {
NavigationView {
Form {
Section(header: Text("List")) {
ForEach(cart.orders) { order in
Text(order.product.name)
}
.onDelete(perform: { indexSet in
deleteProducts(at: indexSet)
})
}
}
.navigationBarTitle(Text("Logbuch"), displayMode: .automatic)
.navigationBarItems(trailing: DateView(date: $date))
}
}
}
struct LogbookView_Previews: PreviewProvider {
static var previews: some View {
LogbookView().environmentObject(Cart())
}
}
I'm using a AppTab View to navigate the app. Therefore, I changed the AppTab View in the main Struct to the default View with an environment object of Cart.
#main
struct KalorientrackerApp: App {
var body: some Scene {
WindowGroup {
AppTabView().environmentObject(Cart())
}
}
}
struct KalorientrackerApp_Previews: PreviewProvider {
static var previews: some View {
Text("Hello, World!")
}
}
I'm opening my AmountView using a .sheet
struct ProductDetailView: View {
#State private var showAmountView = false
let product: Product
var body: some View {
VStack {
// placeholder Image
Image(product.fullImage)
.clipShape(Circle())
.padding(.top, 5)
Spacer()
Form {
productNutritionCollectionView(product: product, modifier: 100)
}
}
// Titel for Navigation bar
.navigationBarTitle(Text(product.name), displayMode: .inline)
// Button to go to amount view
.navigationBarItems(trailing: Button(action: {
self.showAmountView = true
}, label: {
Image(systemName: "plus.circle")
.padding(.leading, 20)
}).sheet(isPresented: $showAmountView, content: {
AmountView(product: product).environmentObject(Cart())
}))
}
}
struct ProductDetailView_Previews: PreviewProvider {
static var previews: some View {
ProductDetailView(product: Product.exampleProduct) }
}
I already found some other discussions, but they didn't worked for me.
I'm using Xcode 12 beta 6 and iOS14 beta 6
I found the bug myself. The problem was, that I committed explicit an .environmentObject in my .sheet action.
AmountView(product: product).environmentObject(Cart())
I removed .environmentObject(Cart()) from the .sheet action. Now it's working.
Thinking this caused the bug because I'm using the .environmentObject(Cart()) operator in the main View of my project.

How to use NavigationLink to detect name of click NavigationView item

How can the destination of a NavigationLink can modified to detect which item within a list was clicked? An if statement of some sort would be very helpful but all items currently do the same thing.
import SwiftUI
let textTitle = NSLocalizedString("PageTitle", comment: "")
let textItemA = NSLocalizedString("ItemA", comment: "")
let textItemB = NSLocalizedString("ItemB", comment: "")
let textItemC = NSLocalizedString("ItemC", comment: "")
struct ItemMain: Identifiable {
var id = UUID()
var name: String
}
let myItems: [MyItem] = [
MyItem(name: textItemA),
MyItem(name: textItemB),
MyItem(name: textItemC)]
struct ContentView: View {
#State private var showingAlert = false
var body: some View {
NavigationView{
List(myItems) { myItem in
NavigationLink(destination: MyDetailView(myItem: myItem)) {
Text(myItem.name)
}
}
.navigationBarTitle(textTitle)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct MyDetailView: View {
var myItem: MyItem
var body: some View {
Text(myIem.name)
}
}
I customized your MyItem model as it is not provided by you
enum MyItemType {
case navigation
case alert
}
struct MyItem: Identifiable {
let id = UUID()
let name: String
var type: MyItemType = .alert
}
And made changed to your ContentView
struct ContentView: View {
#State private var showingAlert = false
var body: some View {
NavigationView{
listView
.navigationBarTitle(textTitle)
}
.alert(isPresented: $showingAlert) {
//Customise your alert according to your need
Alert(title: Text("Alert"))
}
}
private var listView: some View {
List(myItems) { myItem in
if myItem.type == .navigation {
NavigationLink(destination: MyDetailView(myItem: myItem)) {
Text(myItem.name)
}
} else {
Button(action: {
self.showingAlert = true
}) {
Text(myItem.name)
}
}
}
}
}
And lastly your myItems array:
let myItems: [MyItem] = [
MyItem(name: textItemA, type: MyItemType.navigation),
MyItem(name: textItemB),
MyItem(name: textItemC)
]
You can use if statement inside the ForEach to specify special cases:
List(myItems) { myItem in
if (myItem.name == "ItemA") //<<< Here goes your if
{
NavigationLink(destination: MyDetailView(myItem: myItem)) {
Text(myItem.name)
}
}
else
{
//show your alert here

List reload animation glitches

So I have a list that changes when user fill in search keyword, and when there is no result, all the cells collapse and somehow they would fly over to the first section which looks ugly. Is there an error in my code or is this an expected SwiftUI behavior? Thanks.
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = ViewModel(photoLibraryService: PhotoLibraryService.shared)
var body: some View {
NavigationView {
List {
Section {
TextField("Enter Album Name", text: $viewModel.searchText)
}
Section {
if viewModel.libraryAlbums.count > 0 {
ForEach(viewModel.libraryAlbums) { libraryAlbum -> Text in
let title = libraryAlbum.assetCollection.localizedTitle ?? "Album"
return Text(title)
}
}
}
}.listStyle(GroupedListStyle())
.navigationBarTitle(
Text("Albums")
).navigationBarItems(trailing: Button("Add Album", action: {
PhotoLibraryService.shared.createAlbum(withTitle: "New Album \(Int.random(in: 1...100))")
}))
}.animation(.default)
}
}
1) you have to use some debouncing to reduce the needs to refresh the list, while typing in the search field
2) disable animation of rows
The second is the hardest part. the trick is to force recreate some View by setting its id.
Here is code of simple app (to be able to test this ideas)
import SwiftUI
import Combine
class Model: ObservableObject {
#Published var text: String = ""
#Published var debouncedText: String = ""
#Published var data = ["art", "audience", "association", "attitude", "ambition", "assistance", "awareness", "apartment", "artisan", "airport", "atmosphere", "actor", "army", "attention", "agreement", "application", "agency", "article", "affair", "apple", "argument", "analysis", "appearance", "assumption", "arrival", "assistant", "addition", "accident", "appointment", "advice", "ability", "alcohol", "anxiety", "ad", "activity"].map(DataRow.init)
var filtered: [DataRow] {
data.filter { (row) -> Bool in
row.txt.lowercased().hasPrefix(debouncedText.lowercased())
}
}
var id: UUID {
UUID()
}
private var store = Set<AnyCancellable>()
init(delay: Double) {
$text
.debounce(for: .seconds(delay), scheduler: RunLoop.main)
.sink { [weak self] (s) in
self?.debouncedText = s
}.store(in: &store)
}
}
struct DataRow: Identifiable {
let id = UUID()
let txt: String
init(_ txt: String) {
self.txt = txt
}
}
struct ContentView: View {
#ObservedObject var search = Model(delay: 0.5)
var body: some View {
NavigationView {
VStack(alignment: .leading) {
TextField("filter", text: $search.text)
.padding(.vertical)
.padding(.horizontal)
List(search.filtered) { (e) in
Text(e.txt)
}.id(search.id)
}.navigationBarTitle("Navigation")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and i am happy with the result

SwiftUI Picker in a Form doesn't show the selected row

I am trying to have a Picker that shows which option is currently selected.
Try out the following code which correctly selects the right option but the picker does not show which option is selected:
import SwiftUI
struct ContentView: View {
#State var selectedIndex: Int = 0
let strings: [String] = {
var strings: [String] = []
for i in 0..<10 {
strings.append("\(i)")
}
return strings
}()
var body: some View {
NavigationView {
VStack {
Form {
Picker(selection: $selectedIndex,
label: Text("Selected string: \(strings[selectedIndex])")) {
ForEach(0..<strings.count) {
Text(self.strings[$0]).tag($0)
}
}
}
}
.navigationBarTitle("Form Picker",
displayMode: NavigationBarItem.TitleDisplayMode.inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Anyone know what could be wrong? It's observed using Xcode 11.1 and iOS 13.1
I created the simple picker I call "ListPicker" which should fit the bill. I've written it so it works well in a Form; if you need it outside of a Form you will have to tinker with it. If you see any way to improve the code, please add a comment; this is still a learning experience for all of us.
// MARK: - LIST PICKER (PUBLIC)
struct ListPicker<Content: View>: View {
#Binding var selectedItem: Int
var label: () -> Content
var data: [Any]
var selectedLabel: String {
selectedItem >= 0 ? "\(data[selectedItem])" : ""
}
var body: some View {
NavigationLink(destination: ListPickerContent(selectedItem: self.$selectedItem, data: self.data)) {
ListPickerLabel(label: self.label, value: "\(self.selectedLabel)")
}
}
}
// MARK: - INTERNAL
private struct ListPickerLabel<Content: View>: View {
let label: () -> Content
let value: String
var body: some View {
HStack(alignment: .center) {
self.label()
Spacer()
Text(value)
.padding(.leading, 8)
}
}
}
private struct ListPickerContentItem: View {
let label: String
let index: Int
let isSelected: Bool
var body: some View {
HStack {
Text(label)
Spacer()
if isSelected {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}.background(Color.white) // so the entire row is selectable
}
}
private struct ListPickerContent: View {
#Environment(\.presentationMode) var presentationMode
#Binding var selectedItem: Int
var data: [Any]
var body: some View {
List {
ForEach(0..<data.count) { index in
ListPickerContentItem(label: "\(self.data[index])", index: index, isSelected: index == self.selectedItem).onTapGesture {
self.selectedItem = index
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
}
Then you can use it like this:
#State var selectedCar: Int = 0
let cars = ["Jaguar", "Audi", "BMW", "Land Rover"]
Form {
ListPicker(
selectedItem: self.$selectedCar,
label: {
Text("Cars")
},
data: self.cars
)
}

Resources