After long time developing in Swift with storyboard I decide to move to Swift UI.
I make a small project to do that. This project is to simply read a CSV file and show some informations in a List and make this list searchable.
I've already been able to read the CSV file and then open some informations in a List. I've understand approximately how the protocol work but I'm definitely looked with the searching function ...
Here is my "mainView" code :
struct AirportView: View {
#State private var searchText = ""
#ObservedObject var airportsVM:AirportsViewModel = AirportsViewModel()
var body: some View {
let airportsDB = airportsVM.ListAirports
NavigationView {
List{
Section(header: Text("Suisse")){
Text("Hello World")
}
Section(header: Text("France")) {
ForEach(airportsDB, id:\.self) { listAirport in
NavigationLink(destination: Text(listAirport.ApType)) {
Text("\(listAirport.Icao) - \(listAirport.Name)")
}
}
}
}
.id(UUID())
}
.navigationTitle("Airports")
.searchable(text: $searchText)
.navigationViewStyle(StackNavigationViewStyle())
}
and here is my "CSV Reader" Code
import Foundation
struct Airports2: Identifiable, Codable, Hashable{
var AirportID: String = ""
var Icao: String = ""
var ApType: String = ""
var Name: String = ""
var Latitude: String = ""
var Longitude: String = ""
var Altitude: String = ""
var Continent: String = ""
var Country: String = ""
var Region: String = ""
var Municipality: String = ""
var Service: String = ""
var GpsCode: String = ""
var Iata: String = ""
var LocalCode: String = ""
var Link: String = ""
var wikiLink: String = ""
var Keyword: String = ""
let id = UUID()
}
var airportsDB = [Airports2]()
class AirportsClass {
static let
bundleURL = Bundle.main.url(forResource: "FR_Airports", withExtension: "csv")!
static func retrieveAP() -> [Airports2] {
guard let data = try? Data(contentsOf: bundleURL) else {
fatalError("Unable to load airports data")
}
let decoder = String(data: data, encoding: .utf8)
if let dataArr = decoder?.components(separatedBy: "\n").map({ $0.components(separatedBy: ",") })
{
var i = 0
for line in dataArr
{
i += 1
if i < dataArr.count {
let item = Airports2(AirportID: line[0], Icao: line[1], ApType: line[2], Name: line[3], Latitude: line[4], Longitude: line[5], Altitude: line[6], Continent: line[7], Country: line[8], Region: line[9], Municipality: line[10], Service: line[11], GpsCode: line[12], Iata: line[13], LocalCode: line[14], Link: line[15], wikiLink: line[16], Keyword: line[17])
airportsDB.append(item)
}
}
}
return airportsDB
}
}
class AirportsViewModel: NSObject, ObservableObject {
#Published var ListAirports = [Airports2]()
override init() {
ListAirports = AirportsClass.retrieveAP()
}
}
Thanks for your help !
struct AirportView: View {
#State private var searchText = ""
private let airportsVM = AirportsClass.retrieveAP()
var filteredResults: [Airports2] {
if searchText.isEmpty {
return airportsVM.ListAirports
} else {
// some sort of filtering - you'll probably need to filter on multiple properties
return airportsVM.ListAirpots.filter { $0.Name.contains(searchText) }
}
}
var body: some View {
NavigationView {
List{
Section(header: Text("Suisse")){
Text("Hello World")
}
Section(header: Text("France")) {
ForEach(filteredResults, id:\.self) { airport in
NavigationLink(destination: Text(airport.ApType)) {
Text("\(airport.Icao) - \(airport.Name)")
}
}
}
}
.id(UUID())
}
.navigationTitle("Airports")
.searchable(text: $searchText)
.navigationViewStyle(StackNavigationViewStyle())
}
}
Related
This is my data structure
struct SPPWorkout: Codable {
static let setKey = "Sets"
static let exerciseID = "id"
var id: Double? = 0.0
var duration: String?
var calories: Int?
var date: String?
var exercises: [ExerciseSet]
[...]
}
struct ExerciseSet: Codable {
let id: String
let name: String
var reps: Int
var weight: Double
[...]
}
extension ExerciseSet: Equatable {
static func ==(lhs: ExerciseSet, rhs: ExerciseSet) -> Bool {
return lhs.id == rhs.id
}
}
and in a SwiftUI view I'm trying to modify an ExerciseSet from user input
#State private var sppWorkout: SPPWorkout!
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
guard let editingIndex = editingIndex else { return }
sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
sppWorkout.exercises[editingIndex].weight = Double(weight) ?? 0.0
self.editingIndex = nil
})
}
The issue is here
sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
sppWorkout.exercises[editingIndex].weight = Double(weight) ??
and I've tried in all ways to update it, both from the view and with a func in SPPWorkout. I've also tried to replace the object at index
var newSet = ExerciseSet(id: [...], newValues)
self.exercises[editingIndex] = newSet
but in no way it wants to update. I'm sure that somewhere it creates a copy that it edits but I have no idea why and how to set the new values.
Edit: if I try to delete something, it's fine
sppWorkout.exercises.removeAll(where: { $0 == sppWorkout.exercises[index]})
Edit 2:
It passes the guard statement and it does not change the values in the array.
Edit 3:
At the suggestion below from Jared, I've copied the existing array into a new one, set the new values then tried to assign the new one over to the original one but still, it does not overwrite.
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
print(sppWorkout.exercises)
guard let editingIndex = editingIndex else { return }
var copyOfTheArray = sppWorkout.exercises
copyOfTheArray[editingIndex].reps = Int(reps) ?? 0
copyOfTheArray[editingIndex].weight = Double(weight) ?? 0.0
//Copy of the array is updated correctly, it has the new values
sppWorkout.exercises = copyOfTheArray
//Original array doesn't get overwritten. It still has old values
self.editingIndex = nil
Edit 4: I've managed to make progress by extracting the model into a view model and updating the values there. Now the values get updated in sppWorkout, but even though I call objectWillChange.send(), the UI Update doesn't trigger.
full code:
class WorkoutDetailsViewModel: ObservableObject {
var workoutID: String!
#Published var sppWorkout: SPPWorkout!
func setupData(with workoutID: String) {
sppWorkout = FileIOManager.readWorkout(with: workoutID)
}
func update(_ index: Int, newReps: Int, newWeight: Double) {
let oldOne = sppWorkout.exercises[index]
let update = ExerciseSet(id: oldOne.id, name: oldOne.name, reps: newReps, weight: newWeight)
sppWorkout.exercises[index] = update
self.objectWillChange.send()
}
}
struct WorkoutDetailsView: View {
var workoutID: String!
#StateObject private var viewModel = WorkoutDetailsViewModel()
var workout: HKWorkout
var dateFormatter: DateFormatter
#State private var offset = 0
#State private var isShowingOverlay = false
#State private var editingIndex: Int?
#EnvironmentObject var settingsManager: SettingsManager
#Environment(\.dismiss) private var dismiss
var body: some View {
if viewModel.sppWorkout != nil {
VStack {
ListWorkoutItem(workout: workout, dateFormatter: dateFormatter)
.padding([.leading, .trailing], 10.0)
List(viewModel.sppWorkout.exercises, id: \.id) { exercise in
let index = viewModel.sppWorkout.exercises.firstIndex(of: exercise) ?? 0
DetailListSetItem(exerciseSet: viewModel.sppWorkout.exercises[index], set: index + 1)
.environmentObject(settingsManager)
.swipeActions {
Button(role: .destructive, action: {
viewModel.sppWorkout.exercises.removeAll(where: { $0 == viewModel.sppWorkout.exercises[index]})
} ) {
Label("Delete", systemImage: "trash")
}
Button(role: .none, action: {
isShowingOverlay = true
editingIndex = index
} ) {
Label("Edit", systemImage: "pencil")
}.tint(.blue)
}
}
.padding([.leading, .trailing], -30)
//iOS 16 .scrollContentBackground(.hidden)
}
.overlay(alignment: .bottom, content: {
editOverlay
.animation(.easeInOut (duration: 0.5), value: isShowingOverlay)
})
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action : {
do {
try FileIOManager.write(viewModel.sppWorkout, toDocumentNamed: "\(viewModel.sppWorkout.id ?? 0).json")
} catch {
Debugger.log(error: error.localizedDescription)
}
dismiss()
}){
Image(systemName: "arrow.left")
})
} else {
Text("No workout details found")
.italic()
.fontWeight(.bold)
.font(.system(size: 35))
.onAppear(perform: {
viewModel.setupData(with: workoutID)
})
}
}
#ViewBuilder private var editOverlay: some View {
if isShowingOverlay {
ZStack {
Button {
isShowingOverlay = false
} label: {
Color.clear
}
.edgesIgnoringSafeArea(.all)
VStack{
Spacer()
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
guard let editingIndex = editingIndex else { return }
print(viewModel.sppWorkout.exercises)
print("dupa aia:\n")
viewModel.update(editingIndex, newReps: Int(reps) ?? 0, newWeight: Double(weight) ?? 0.0)
print(viewModel.sppWorkout.exercises)
self.editingIndex = nil
})
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color("popupBackground"),
lineWidth: 3)
)
}
}
}
}
}
So I got a very good explanation on reddit on what causes the problem. Thank you u/neddy-seagoon if you are reading this.
The explanation
. I believe that updating an array will not trigger a state update. The only thing that will, with an array, is if the count changes. So
sppWorkout.exercises[index].reps = newReps
will not cause a trigger. This is not changing viewModel.sppWorkout.exercises.indices
So all I had to to was modify my List from
List(viewModel.sppWorkout.exercises, id: \.id)
to
List(viewModel.sppWorkout.exercises, id: \.hashValue)
as this triggers the list update because the hashValue does change when updating the properties of the entries in the list.
For the line
List(viewModel.sppWorkout.exercises, id: \.id) { exercise in
Replace with
List(viewModel.sppWorkout.exercises, id: \.self) { exercise in
I have this code:
struct Restaurants: Identifiable {
let name: String
let imageUrl: URL
let id: String
let rating: Double
let url: String
let category: String
static var viewModels: [Restaurants] = [
Restaurants(name: "Silverio's Mexican Kitchen", imageUrl: URL(string: "https://mainsite-prod-cdn.azureedge.net/partner-images/432257/micrositeimage_p1.jpg")!, id: "hjkhjhjh", rating: 2, url: "https://google.com"),//, category: "Mexican"),
Restaurants(name: "Taqueria La Esquinita", imageUrl: URL(string: "https://s3-media0.fl.yelpcdn.com/bphoto/x-KCQ7osmvBWLA9WpPdO_Q/o.jpg")!, id: "hjdha", rating: 3, url: "https://google.com")//, category: "Mexican")
] {
didSet {
print("data set")
}
}
}
How do I store viewModels in Firestore? I want to maintain this object structure so that I can carry it across multiple devices.
The idea of this app is to help groups find a place to eat. Therefore, I have this structure:
I am now using sub-collections for each Restaurant thanks to #FaridShumbar. How do I get documents from a sub-collection?
You need the get() method in order to retrieve documents.
Following examples from the documentation, you could retrieve your documents as follows:
db.collection("parties").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
}
}
}
This example retrieves all documents in a collection.
I did it by saving each element in the struct separately and then putting it back together when I get the document.
Save Code:
db.collection("parties").document(Utilities.code).collection("Restaurants").document(card.name).setData([
"name" : card.name,
"img" : imgUrl,
"id" : card.id,
"rating" : card.rating,
"url" : card.url,
"yes" : FieldValue.arrayUnion([Utilities.name])
], merge: true) { error in
if error != nil {
print("error adding restaurant: \(error!)")
}
}
Retrieve code:
class restaurantsFirebase: ObservableObject {
#Published var restaurants: [RestaurantListViewModel] = []
private let db = Firestore.firestore()
private var devices = Array<String>()
func fetchData() {
db.collection("parties").document(Utilities.code).addSnapshotListener { doc, error in
if error == nil {
if doc != nil && doc!.exists {
if let getDevices = doc!.get("devices") as? Array<String> {
self.devices = getDevices
}
}
}
}
db.collection("parties").document(Utilities.code).collection("Restaurants").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("no documents")
return
}
self.restaurants = documents.compactMap({ (queryDocumentSnapshot) in
print(queryDocumentSnapshot.data())
let data = queryDocumentSnapshot.data()
let name = data["name"] as? String ?? ""
let img = data["img"] as? String ?? ""
let id = data["id"] as? String ?? ""
let rating = data["rating"] as? Double ?? 0
let url = data["url"] as? String ?? ""
let yes = data["yes"] as? Array<String> ?? []
let compiled = RestaurantListViewModel(name: name, imageUrl: URL(string: img)!, id: id, rating: rating, url: url)
print("restaurant = \(compiled), restaurants = \(self.restaurants)")
print("yes = \(yes), devices = \(self.devices)")
if yes == self.devices {
// self.restaurants.append(compiled)
return compiled
}else {
return nil
}
})
}
}
}
Use:
#ObservedObject private var viewModels = restaurantsFirebase()
db.collection("parties").document(Utilities.code).addSnapshotListener { doc, error in
if error == nil {
if doc != nil && doc!.exists {
if let dev = doc!.get("devices") as? Array<String> {
print("devices = \(dev)")
devices = dev
}
if let done = doc!.get("devicesDone") as? Array<String> {
print("devicesDone = \(done), devices = \(devices)")
if done == devices {
self.viewModels.fetchData()
showFinal = true
results = false
}
}
}
}
I'm building a feed, where I want to unite two different structures and sort the feed by date, here's how I've tried. It shows mistakes in View. It writes either "compiler unable to type check in reasonable time", either something else that connects to the ForEach loop.
Maybe you have an idea what can be an issue? Or, if there's other way to build and sort the feed?
EDIT: The following error: "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
class FeedViewModel: ObservableObject {
#Published var feed: [FeedCommonContent] = []
// download(ed) articles
self.feed.append(Article(id: doc.document.documentID, title: title, pic: pic, time: time.dateValue(), user: user))
// download(ed) tutorials
self.feed.append(Tutorial(id: doc.document.documentID, title: title, steps: [steps], notes: notes, pic: pic, time: time.dateValue(), user: user))
// sort the feed
self.feed.sort { (p1, p2) -> Bool in
return p1.time > p2.time
}
}
protocol FeedCommonContent {
// Define anything in common between objects
var time: Date { get set }
var id: String { get set }
var feedIdentity: FeedIdentity.RawValue { get } // Article, or Tutorial; or - { get set }
}
enum FeedIdentity: String {
// case Article, Tutorial
case Article = "Article"
case Tutorial = "Tutorial"
}
struct Article: Identifiable, FeedCommonContent {
var feedIdentity = FeedIdentity.Article.rawValue
var id: String = UUID().uuidString
var title: String
var pic: String
var time: Date
var user: User
}
struct Tutorial: Identifiable, FeedCommonContent {
var feedIdentity = FeedIdentity.Tutorial.rawValue
var id: String = UUID().uuidString
var titleImage: String
var name: String
var user: User
var inventory: [String]
var steps: [String]
var notes: String
var time: Date
var warnings: String
}
struct FeedView: View {
#StateObject var feedData = FeedViewModel()
var body: some View {
ScrollView {
ForEach(feedData.feed, id: \.self.id) { item in
// also tried type checking:
// if item is Article // or, if let article = item as? Article
if item.feedIdentity == FeedIdentity.Article.rawValue {
NavigationLink(destination: ArticleView(article: item, articleData: feedData)) {
ArticleUnitView(article: item, feedData: feedData)
}
} else if item.feedIdentity == FeedIdentity.Tutorial.rawValue { // if item is Tutorial
NavigationLink(destination: TutorialView(tutorial: item, feedData: feedData)) {
TutorialUnitView(tutorial: item, feedData: feedData)
}
}
}
}
When I did an array only of one of data structures, all worked. (i.e. either var articles: [Article] = [], or var tutorials: [Tutorial] = [] etc..)
this is the (test) code I used to show how to "sort" your feed by date, and display it in a View:
EDIT, using "Joakim Danielson" enum suggestion:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
FeedView()
}
}
}
class FeedViewModel: ObservableObject {
#Published var feed: [FeedCommonContent] = []
// for testing
init() {
self.feed.append(Article(title: "article title", pic: "pic1", time: Date(), user: User()))
self.feed.append(Tutorial(titleImage: "title1", name: "tut1", user: User(), inventory: [], steps: [], notes: "note1", time: Date(), warnings: ""))
sortFeed()
}
func sortFeed() {
feed.sort { $0.time > $1.time }
}
}
protocol FeedCommonContent {
// Define anything in common between objects
var time: Date { get set }
var id: String { get set }
var feedIdentity: FeedIdentity { get } // Article, or Tutorial; or - { get set }
}
enum FeedIdentity: String {
// case Article, Tutorial
case Article
case Tutorial
}
struct User: Identifiable {
var id: String = UUID().uuidString
var name: String = "user"
}
struct Article: Identifiable, FeedCommonContent {
let feedIdentity = FeedIdentity.Article
var id: String = UUID().uuidString
var title: String
var pic: String
var time: Date
var user: User
}
struct Tutorial: Identifiable, FeedCommonContent {
let feedIdentity = FeedIdentity.Tutorial
var id: String = UUID().uuidString
var titleImage: String
var name: String
var user: User
var inventory: [String]
var steps: [String]
var notes: String
var time: Date
var warnings: String
}
struct FeedView: View {
#StateObject var feedData = FeedViewModel()
var body: some View {
ScrollView {
ForEach(feedData.feed, id: \.id) { item in
// switch item.feedIdentity {
// case FeedIdentity.Article:
// NavigationLink(destination: ArticleView(article: item, articleData: feedData)) {
// ArticleUnitView(article: item, feedData: feedData)
// }
// case FeedIdentity.Tutorial:
// NavigationLink(destination: TutorialView(tutorial: item, feedData: feedData)) {
// TutorialUnitView(tutorial: item, feedData: feedData)
// }
// }
// for testing
switch item.feedIdentity {
case FeedIdentity.Article:
NavigationLink(destination: Text(item.feedIdentity.rawValue + " " + item.id)) {
Text(item.feedIdentity.rawValue)
}
case FeedIdentity.Tutorial:
NavigationLink(destination: Text(item.feedIdentity.rawValue + " " + item.id)) {
Text(item.feedIdentity.rawValue)
}
}
}
}
}
I have created a New View in my App, but the Identifiable Object won't append to the Array.
I really don't know why its not appending...
Here is the Code:
struct FirstSettingsIdentifiables: Identifiable {
var id: UUID = UUID()
var name: String
var icon: String
}
struct SettingsView: View {
#EnvironmentObject var settingItems: ContentModel
#State var firstArr: [FirstSettingsIdentifiables] = []
init() {
createFirstList()
print("Settings successfully initialized.")
}
var body: some View {
return VStack {
Text("Einstellungen")
.font(.title)
NavigationView {
//Mitteilungen Liste
List(firstArr) { x in
// ForEach(firstArr) { x in
// VStack {
// Image(systemName: x.icon)
Text("Das ist ein test")
// }
// }
}.navigationBarTitle("Mitteilungen")
}
}
}
func createFirstList() {
let aText = "Mitteilungen"
let aIcon = "info.circle.fill"
let aObject = FirstSettingsIdentifiables(name: aText, icon: aIcon)
firstArr.append(aObject)
print(firstArr.count)
}
}
The problem is probably in the createFirstList() Section. In this function, the Object aObject is full of data(This is working fine), but then the Object won't append to my firstArr. The count is always 0.
What am I doing wrong here?
You are changing the value of firstArr too early. Instead of calling createFirstList() in the init, remove that and instead add the following code onto the view body:
VStack {
/* ... */
}
.onAppear(perform: createFirstList)
Alternatively, you could do the following:
init() {
_firstArr = State(initialValue: getFirstList())
print(firstArr.count)
print("Settings successfully initialized.")
}
/* ... */
func getFirstList() -> [FirstSettingsIdentifiables] {
let aText = "Mitteilungen"
let aIcon = "info.circle.fill"
let aObject = FirstSettingsIdentifiables(name: aText, icon: aIcon)
return [aObject]
}
I am trying to find a string within an array using this code;
var userArray = [UserItem]()
var foundUser: String {
guard let findUser = userArray.first(where: { $0 == item.name }) else { return "Not found" }
return findUser
}
But I am getting the following error message;
"Cannot convert value of type '(String) -> Bool' to expected argument type '(UserList) throws -> Bool'"
So I tried adding a standard array;
var userArray = ["1", "Gale Dyer", "3", "4"]
and got rid of the error and the result I intended.
I assume it is because my struct or class does not conform to String but I am not sure how I fix this as adding ", String" doesn't seem to be the answer.
For reference here is the other data;
struct UserItem: Codable, Identifiable {
var id: String
var isActive: Bool
var name: String
var age: Int
var company: String
var email: String
var address: String
var about: String
// var registered: Date
var tags: [String]
var friends: [Friend]
}
struct Friend: Codable {
var id: String
var name: String
}
class UserList: ObservableObject {
#Published var items = [UserItem]()
{
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([UserItem].self, from: items) {
self.items = decoded
return
}
}
self.items = []
}
}
Thank you in advance
You have to check with same type in the closure of first(where:) method. Here's the fix.
var foundUser: String {
guard let findUser = userArray.first(where: { $0.name == item.name })?.name else { return "Not found" }
return findUser
}