I have previous coding experience but I'm extremely new to both Swift and iOS. I'm developing an app for personal use to coalesce the functions of multiple different apps I use to help manage ADHD into one place. The main view of the app provides a way to track daily expenditures. I'm trying to use UserDefaults to store the information entered so it will still be there when I reopen the app. The app runs fine, and as near as I can figure out I've written the data handling right, but it simply doesn't work. I've been beating my head against the wall at this for the past few days including on this website, so any help would be greatly appreciated. Here's the code for the main view of the app:
import SwiftUIFontIcon
import SwiftUI
//import UIKit
struct ContentView: View {
#State public var purchases = [Purchases]()
#State public var prices = [Price]()
#State public var isActive = false
#State public var goTo: String = ""
#State public var purchase: String = ""
#State public var price: String = ""
// #State public var isActive: Bool = false
init(){
if let data = UserDefaults.standard.data(forKey: "Purchases"){
if let decoded = try? JSONDecoder().decode([Purchases].self, from: data){
self.purchases = decoded
}
return
}
self.purchases = []
if let data2 = UserDefaults.standard.data(forKey: "Bread"){
if let decoded2 = try? JSONDecoder().decode([Price].self, from: data2){
self.prices = decoded2
}
return
}
self.prices = []
}
func addItem(){
saveStuff()
self.purchases.append(Purchases(name: purchase))
saveStuff()
purchase = ""
}
func addPrice(){
saveBread()
self.prices.append(Price(name: price))
saveBread()
price = ""
}
func deleteItem(at offsets: IndexSet){
purchases.remove(atOffsets: offsets)
}
func deletePrice(at offsets: IndexSet){
prices.remove(atOffsets: offsets)
}
func saveStuff(){
if let encodedData = try? JSONEncoder().encode(purchases){
UserDefaults.standard.set(encodedData, forKey: "Purchases")
}
// return
}
func saveBread(){
if let encodedData = try? JSONEncoder().encode(prices){
UserDefaults.standard.set(encodedData, forKey: "Bread")
}
// return
}
func clearList(){
self.prices.removeAll()
self.purchases.removeAll()
}
var body: some View {
NavigationView{
ZStack {
Color.gray
.edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
VStack {
// Spacer()
// HStack {
// // Spacer()
// NavigationLink(
// destination: ToDoList(rootIsActive: self.$isActive),
// isActive: self.$isActive
//
// ){
// FontIcon.text(.ionicon(code: .ios_list_box), fontsize: 48, color: .black)
// }
// Spacer()
// NavigationLink(destination: ReminderView()){
// FontIcon.text(.ionicon(code: .ios_warning), fontsize: 48, color: .black)
// }
// // Spacer()
// }
Spacer()
VStack{
Spacer()
HStack {
Spacer()
HStack {
Spacer()
TextField("Add an Item", text: $purchase)
.padding(12)
.border(Color.black)
Spacer()
Spacer()
Spacer()
TextField("Add a Price", text: $price)
.padding(12)
.border(Color.black)
// Spacer()
FontIcon.button(.ionicon(code: .ios_add_circle), action: {
addItem()
addPrice()
saveStuff()
saveBread()
}, padding: 12, fontsize: 45, color: .black)
}
.opacity(1)
.padding(12)
// .border(Color.black)
// Spacer()
}
Spacer()
VStack {
HStack {
Spacer()
List{
ForEach(purchases){ purchase in
Text(purchase.name)
.cornerRadius(16)
.padding(10)
}
.onDelete(perform: deleteItem)
// .listStyle(GroupedListStyle())
}
Spacer()
List{
ForEach(prices){ price in
Text(price.name)
.cornerRadius(16)
.padding(10)
}
.onDelete(perform: deletePrice)
// .onAppear{UITableView.appearance().separatorColor = .clear}
}
Spacer()
}
Spacer()
HStack {
// Spacer()
// FontIcon.button(.ionicon(code: .ios_save), action: {
// saveStuff()
// saveBread()
// }, padding: 12, fontsize: 78, color: .green)
Spacer()
FontIcon.button(.ionicon(code: .ios_trash),action:{
clearList()
}, padding: 12, fontsize: 78, color: .red)
Spacer()
NavigationLink(destination: ViewLists()){
FontIcon.text(.ionicon(code: .ios_filing), fontsize: 58, color: .black)
}
Spacer()
NavigationLink(destination: ViewTotals()){
FontIcon.text(.ionicon(code: .ios_add), fontsize: 58, color: .black)
}
Spacer()
NavigationLink(destination: IncomeView()){
FontIcon.text(.ionicon(code: .ios_musical_note), fontsize: 58, color: .black)
}
Spacer()
}
// Spacer()
}
Spacer()
}
Spacer()
}
.navigationBarTitle("ADD ToolKit", displayMode: .large)
Spacer()
// .navigationBarTitle("Ledger", displayMode: .large)
// Spacer()
}
.toolbar{
ToolbarItem(placement: .primaryAction){
// Spacer()
// Spacer()
HStack {
Spacer()
NavigationLink(destination: ReminderView()){
FontIcon.text(.ionicon(code: .ios_warning), fontsize: 48, color: .black)
// Spacer()
}
}
}
ToolbarItem(placement: .navigationBarLeading){
HStack {
Spacer()
NavigationLink(
destination: ToDoList(rootIsActive: self.$isActive),
isActive: self.$isActive
){
FontIcon.text(.ionicon(code: .ios_list_box), fontsize: 48, color: .black)
}
}
}
}
//
}
.background(
NavigationLink(destination: Text(self.goTo), isActive: $isActive){
EmptyView()
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and the code for the model where I define all the array structs:
import Foundation
import SwiftUI
struct Purchases: Identifiable, Codable{
let id: String
let name: String
init(id: String = UUID().uuidString, name: String){
self.id = id
self.name = name
}
}
struct Price: Identifiable, Codable{
let id: String
let name: String
init(id: String = UUID().uuidString, name: String){
self.id = id
self.name = name
}
}
struct ToDo: Identifiable, Codable{
let id: String
let name: String
init(id: String = UUID().uuidString, name: String){
self.id = id
self.name = name
}
}
struct Reminder: Identifiable, Codable{
let id: String
let name: String
init(id: String = UUID().uuidString, name: String){
self.id = id
self.name = name
}
}
struct Income: Identifiable, Codable{
let id: String
let name: String
init(id: String = UUID().uuidString, name: String){
self.id = id
self.name = name
}
}
Thanks in advance
Add the below code to a .swift file in your project
//Allows all Codable Arrays to be saved using AppStorage
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
Then use #AppStorage vs #State for the arrays you want saved
#AppStorage("Purchases") var purchases: [Purchases] = []
#AppStorage("Bread") var breadPrices: [Price] = []
You can write to them as you would to a regular array
You don't need saveStuff or saveBread
Below is a simplified version of your code. I couldn't reproduce yours to test.
import SwiftUI
struct CodableUserDefaultView: View {
#AppStorage("Purchases") var purchases: [Purchases] = []
//#State public var purchases = [Purchases]()
#AppStorage("Bread") var breadPrices: [Price] = []
//#State public var breadPrices = [Price]()
#State public var isActive = false
#State public var goTo: String = ""
#State public var price: String = ""
func addItem(purchaes: Purchases){
self.purchases.append(purchaes)
}
func addPrice(price: Price){
self.breadPrices.append(price)
}
func deleteItem(at offsets: IndexSet){
purchases.remove(atOffsets: offsets)
}
func deletePrice(at offsets: IndexSet){
breadPrices.remove(atOffsets: offsets)
}
func clearList(){
self.breadPrices.removeAll()
self.purchases.removeAll()
}
var body: some View {
List{
Section(content: {
ForEach(breadPrices){ price in
HStack{
Text(price.name)
Spacer()
Button("purchase", action: {
addItem(purchaes: Purchases(name: price.name))
})
}
}.onDelete(perform: deletePrice)
VStack{
Text("Bread")
TextField("Bread Price", text: $price, onCommit: {
addPrice(price: Price(name: price))
})
}
}, header: {
Text("Bread")
})
Section(content: {
ForEach(purchases){ purchase in
HStack{
Text(purchase.name)
}
}.onDelete(perform: deleteItem)
}, header: {
Text("Purchases")
})
}
}
}
struct CodableUserDefaultView_Previews: PreviewProvider {
static var previews: some View {
CodableUserDefaultView()
}
}
But as mentioned in the comments this really isn't a good use for UserDefaults. It is meant for smaller stuff.
The defaults system allows an app to customize its behavior to match a user’s preferences. For example, you can allow users to specify their preferred units of measurement or media playback speed. Apps store these preferences by assigning values to a set of parameters in a user’s defaults database.
https://developer.apple.com/documentation/foundation/userdefaults
You might want to look into Core Data if you are staying iOS only or another Database system like Firebase, AWS, Azure, etc.
Related
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)
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)
}
}
I want the Images to change according to the text, but it works over .degisim String and int value in the ready-made code block I found. That's why it doesn't work for String.
import SwiftUI
struct altin: View {
#State var users: [Altin] = []
var body: some View {
ZStack{
List(users) { altin in
NavigationLink {
detayView(altinKur: altin)
} label: {
HStack{
Image(systemName: getImage(percent: Int(altin.degisim)))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 9, height: 9)
.foregroundColor(
(altin.degisim).contains("-") ?
Color.down :
Color.up)
.padding(.trailing, 5)
Text(altin.degisim)
.font(.system(size: 16))
.fontWeight(.medium)
.foregroundColor(
(altin.degisim).contains("-") ?
Color.down :
Color.up)
}
}
}
}
.listStyle(PlainListStyle())
.padding(0)
.onAppear {
apiCall().getUsers { (users) in
self.users = users
}
}
}
}
struct altin_Previews: PreviewProvider {
static var previews: some View {
altin()
}
}
func getImage(percent: Int) -> String{
if percent > 0 {
return "chevron.up"
}else{
return "chevron.down"
}
}
.foregroundcolor works but not within Image.
The code is more complex, I simplified it for solution.
Model:
struct Altin: Codable, Identifiable {
let id = UUID()
let name: String
let alis: String
let satis: String
let degisim: String
}
I'm using SwiftUI and having some problem passing my string array to a view.
Let's me explain the situation. I'm working on a Gallery app to show some artist's paintings.
I create a TabView to show all the paintings from each artist without any problem but I wanted to make each paintings clickable to see the detail view of the painting, and this where I get stuck.
Every time I click on a paintings it's show me the same paintings...
here is the samples code:
Model & View Model
let artistData: [Artist] = [
Artist(
name: "Piotre",
profilePic: "Piotre",
biography: "Piotre, est un...",
worksImages: [
"GO LOVE YOUR SELF 115.5-89",
"GRAFFITI THERAPIE 146-226",
"HELLO MY NAME IS 130-162",
"KING'S GARDEN 162-130",
"LION'S GARDEN 100-100",
"R'S GARDEN 162 130"
],
workName: [
"GO LOVE YOUR SELF",
"GRAFFITI THERAPIE",
"HELLO MY NAME IS",
"KING'S GARDEN",
"LION'S GARDEN",
"R'S GARDEN"
], workSize: [
"115.5-89",
"146-226",
"130-162",
"162-130",
"100-100",
"162 130"
]),
]
struct Artist: Identifiable {
var id = UUID()
var name: String
var profilePic: String
var biography: String
var worksImages: [String]
var workName: [String]
var workSize: [String]
}
struct ArtistGalleryView: View {
//MARK:- PROPERTIES
var work: Artist
//MARK:- BODY
var body: some View {
ZStack {
Color(#colorLiteral(red: 0.6549019608, green: 0.7137254902, blue: 0.862745098, alpha: 1)).opacity(0.2)
.edgesIgnoringSafeArea(.all)
VStack {
//MARK:- Tableaux
TabView {
ForEach(work.worksImages, id: \.self) { works in
Image(works)
.resizable()
.scaledToFit()
}
.padding()
}
.tabViewStyle(PageTabViewStyle())
}
}
.frame(width: 330, height: 400, alignment: .center)
.cornerRadius(10)
}
}
struct GalleryView: View {
//MARK:- PROPERTIES
var artist: [Artist] = artistData
init() {
UINavigationBar.appearance().titleTextAttributes = [.font: UIFont(name: "WorkSans-Bold", size: 20)!]
}
//MARK:- BODY
var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
VStack {
ForEach(artist) { item in
VStack(alignment: .leading) {
ArtistName(artist: item)
NavigationLink(destination: WorksDetailView(work: item)) {
ArtistGalleryView(work: item)
}
}
.buttonStyle(PlainButtonStyle())
}
.padding()
}
}
.navigationBarItems(trailing:Button(action: {
}) {
}
)
.navigationBarTitle(
Text("Gallery"), displayMode: .inline)
}
}
}
And my detail view where I want to see the corresponding image
struct WorksDetailView: View {
#State private var moveUp = false
var work: Artist
var body: some View {
ZStack {
VStack(alignment: .leading) {
Text(work.workName[0])
.modifier(CustomFontModifier(size: 22, name: "WorkSans-Bold"))
.padding(.horizontal)
Text(work.workSize[0])
.modifier(CustomFontModifier(size: 17, name: "WorkSans-Light"))
.foregroundColor(.secondary)
.padding(.horizontal)
VStack {
Image(work.worksImages[0])
.resizable()
.scaledToFit()
}
.padding()
}
.padding(.horizontal)
.padding(.bottom, 150)
Button(action: {
print("Show AR")
}) {
NeumorphicButton(moveUp: $moveUp)
}
.onAppear(perform: {
withAnimation(.easeInOut(duration: 1)) {
moveUp.toggle()
}
})
.offset(y: moveUp ? 225 : 450)
}
}
}
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.