I have a series of videos for the user to choose from. At any point, I want to be able to access the video property by calling Video.setupFull.watched and find out if it's nil or not. I created a struct and then static variables to create each video.
Here is my setup code:
struct Video {
let name: String
let image: String
let firebaseID: String
let number: Int
let url: String
let desc: String
var playbackTime: TimeInterval?
var watched: Bool?
var watchedDate: TimeInterval?
static let setupFull = Video(name: "Setup Full",
image: "Setup Full",
firebaseID: "setupTutorialFull",
number: 300928943,
url: "https://player.vimeo.com/external/282034045",
desc: "Moneypants Setup Tutorial Full")
static let step1Income = Video(name: "Step 1 - Income",
image: "Step 1 Income",
firebaseID: "step1Income",
number: 282034041,
url: "https://player.vimeo.com/external/282034041",
desc: "Moneypants Setup Tutorial (Step 1) - Income")
static let step2FamilyMembers = Video(name: "Step 2 - Family Members",
image: "Step 2 Family Members",
firebaseID: "step2FamilyMembers",
number: 282600740,
url: "https://player.vimeo.com/external/282600740",
desc: "Moneypants Setup Tutorial (Step 2) - Family Members")
static var array = [setupFull, step1Income, step2FamilyMembers]
}
To save space, I only store the optional parameters of the video objects (playbackTime, the watched, and the watchedDate) on Firebase, which looks like this:
{
"setupTutorialFull" : {
"playbackTime" : 194,
"watched" : true,
"watchedDate" : 1550004207
},
"step1Income" : {
"playbackTime" : 10,
"watched" : true,
"watchedDate" : 1544566787
},
"step2FamilyMembers" : {
"playbackTime" : 22,
"watched" : true,
"watchedDate" : 1544589461
}
}
(Note that all the videos in the list have been watched.)
I want to do a batch update when I initially load the data, so I made the array of videos and tried to update them like this:
static func observeDB(completion: #escaping () -> Void) {
FB.ref
.child(FB.videos)
.observe(.value) { (snapshot) in
for item in snapshot.children {
guard let snap = item as? DataSnapshot else { return }
guard let value = snap.value as? [String: Any] else { return }
let videoID = snap.key
let playbackTime = value[FB.playbackTime] as? TimeInterval
let watched = value[FB.watched] as? Bool
let watchedDate = value[FB.watchedDate] as? TimeInterval
// Find the appropriate video and update its variables.
if let index = array.firstIndex(where: { $0.firebaseID == videoID }) {
array[index].playbackTime = playbackTime
array[index].watched = watched
array[index].watchedDate = watchedDate
}
}
completion()
}
}
But, to my dismay, the static variables don't update when the array does.
print(Video.setupFull.watched) // prints `nil`
print(Video.array[0].watched) // prints `true`
Now if I manually modify the static var directly, it updates. This is expected, of course.
Video.setupFull.watched = true
print(Video.setupFull.watched) // prints `true`
The problems is I have potentially hundreds of videos to update, though, and don't want to have to manually update each one. That's why I created the static array. That way I could iterate over the array and update the videos quickly. I want to be able to call Video.setupFull.watched on a separate view controller and it to show watched.
My question: Why don't the static variables update when they are embedded in an array? Why can't I use Video.array[0].watched = true and it update the underlying Video.setupFull.watched object?
I thought that was how structs worked. They don't create copies, but are directly modifiable. What am I missing?
Related
Tried to combine or merging two model to one model
1st model = items [ InboxModel]. (My own Inbox)
2nd model = items2 [MoInboxModel] (SDK Inbox)
1st + 2nd -> combinedItems
private var items: [InboxModel] = []
private var items2: [MoInboxModel] = []
private var combinedItems: [combinedInboxModel] = []
struct InboxModel {
let id: String
let title: String
let message: String
let date: Date
}
struct MoInboxModel {
let id: String
let title: String
let message: String
let date: Date
}
struct combinedInboxModel {
let id: String
let title: String
let message: String
let date: Date
}
self?.combinedItems.append(self?.items). //No exact matches in call to instance method 'append
self?.combinedItems.append(contentsOf: self?.items2 ?? []) //No exact matches in call to instance method 'append
Why there is an error while merge it ? How to merge it correctly?
You have three unrelated types - InboxModel, MoInboxModel and combinedInboxModel (Which should be CombinedInboxModel. Even though they all have properties with the same name, they are different types.
There is no append function on an array of combinedInboxModel that accepts an array of InboxModel or MoInboxModel.
You could use map on each of your two input arrays to convert them to an array of CombinedInboxModel which you can then put into combinedItems.
Presumably you are writing this code in a closure, which is why you have a weak self. Best to deal with that first and then process your arrays.
guard let self = self else {
return
}
self.combinedItems = self.items.map { CombinedInboxModel(id:$0.id,title:$0.title,message:$0.message,date:$0.date) }
let items2 = self.items2.map { CombinedInboxModel(id:$0.id,title:$0.title,message:$0.message,date:$0.date) }
self.combinedItems.append(contentsOf:items2)
You haven't shown where items and items2 come from; Is it possible just to fetch them as instances of the same struct to start with?
The fact that you have three structs with the same properties is a bit fishy. I would consider a different design if I were you.
However, if you must go with this approach, you might want to consider starting with a protocol and getting rid of the combinedInboxModel struct.
protocol InboxModelable {
var id: String { get }
var title: String { get }
var message: String { get }
var date: Date { get }
}
Now make your two structs conform to InboxModelable.
struct InboxModel: InboxModelable {
let id: String
let title: String
let message: String
let date: Date
}
struct MoInboxModel: InboxModelable {
let id: String
let title: String
let message: String
let date: Date
}
Since both of your types conform to InboxModelable you can directly store both types in an array of type Array<InboxModelable> without having to map the elements.
class SomeClass {
private var items: [InboxModel] = []
private var items2: [MoInboxModel] = []
private var combinedItems: [InboxModelable] = []
func combineItems() {
doSomething { [weak self] in
guard let self = self else { return }
self.combinedItems.append(contentsOf: self.items)
self.combinedItems.append(contentsOf: self.items2)
}
}
}
I'm tryin' to obtain a list of activities ("dd/mm/YY: goal achieved/missed goal") which has to be setted every week. The problem is that I obtain a list of activities with the same date and the same result of the previous one. For example:
28/02/2022: goal achieved
28/02/2022: goal achieved
28/02/2022: goal achieved
and the next day:
01/03/2022: missed goal
01/03/2022: missed goal
01/03/2022: missed goal
01/03/2022: missed goal
I want to obtain, instead, a list like:
28/02/2022: goal achieved
01/03/2022: missed goal
02/03/2022: goal achieved...
These are useful structs:
struct Persistent {
#AppStorage("goalAchieved") static var goalAchieved : Bool = false
#AppStorage("activityList") static var activityList : [String] = []
}
struct obj {
static var currentDate = Date()
static var stringDate = ""
static var activity = Activity(date:Persistent.lastUpdatedDate)
}
This is the ActivityListView:
import SwiftUI
func activitystring(activity:Activity) -> String{
var output = ""
output = "\(activity.date): \(activity.reachedobj(goalAchieved: Persistent.goalAchieved))"
return output
}
struct Activity: Identifiable{
let id = UUID()
let date: String
func reachedobj(goalAchieved: Bool) -> String {
var output = ""
if Persistent.goalAchieved == false { output = "Missed goal" }
if Persistent.goalAchieved == true { output = "Goal Achieved!"}
return output
}
}
struct ActivityRow: View{
var activity: Activity
var body: some View{
Text(activitystring(activity: activity))
Divider()
}
}
struct ActivityListView: View {
var body: some View {
ScrollView{
Text("Week summary").font(.system(size: 15)).foregroundColor(Color.green)
Text("")
ForEach(Persistent.activityList, id: \.self) { activity in
let activity = Activity(date: Persistent.lastUpdatedDate)
ActivityRow(activity: activity)
}
}
}
}
Finally this is the useful code in the ApplicationApp file (main) where I update activity list:
MenuView().onAppear(){
if Persistent.activityList.count>7{
Persistent.activityList.removeAll()
}
obj.currentDate = Date()
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/YY"
obj.stringDate = formatter.string(from:obj.currentDate)
if Persistent.lastUpdatedDate != obj.stringDate{
Persistent.goalAchieved = false
let activity = Activity(date: Persistent.lastUpdatedDate)
Persistent.activityList.append(activitystring(activity: activity))
Persistent.lastUpdatedDate = obj.stringDate
}
}
What's wrong on this?
You're calling obj.activity in your ForEach and ActivityRow, that's why it repeats that same static property all over the place.
You better just drop your struct obj and try again without it
In your Persistent object you have an array of many activities, called activitylist , but one single boolean that tells if the goal is achieved - goalachieved indeed.
Your view is iterating through the array of Persistent.activitylist, so you will have many lines for one single result - achieved or not achieved. You might actually want to iterate over an array of Persistent objects - meaning that somewhere you should probably store [Persistent] in some variable. In this way, you will see one line only for each result.
If I also may suggest: use the conventions for naming variables, Swift uses "camelCaseConventionForVariables", easier to read than "thewholevariableislowercase"
Edit:
Let me try to change a little bit your code (I would personally change it more radically, but that's not the scope of the answer).
Instead of having only one goalAchieved for all elements on the array activityList, make it a dictionary:
struct Persistent {
// Drop this variable
// #AppStorage("goalAchieved") static var goalAchieved : Bool = false
// Make this a dictionary, the date will be the key and the goalAchieved will be the value
#AppStorage("activityList") static var activityList : [String: Bool] = [:]
}
Add values to the dictionary (#meomeomeo is right, you don't need obj):
MenuView().onAppear() {
if Persistent.activityList.count > 7 {
Persistent.activityList.removeAll()
}
let currentDate = Date()
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/YY"
let stringDate = formatter.string(from: currentDate)
if Persistent.lastUpdatedDate != stringDate {
let activity = Activity(date: Persistent.lastUpdatedDate)
Persistent.activityList[activitystring(activity: activity))] = false // Will create something like ["01/03/2022": false]
Persistent.lastUpdatedDate = stringDate
}
}
Iterate on the dictionary in your ForEach; for more info: read here.
I'm a Windows C# developer, new to iOS/SwiftUI development and I think I've worked myself into a hole here.
I have a view with a #Binding variable:
struct DetailView: View {
#Binding var project: Project
The project is an object which contains an array of Tasks. I am looping through the tasks of the project to display its name and a toggle whose state is determined by the Task's variable, isComplete.
ForEach(filteredTasks.indices, id: \.self) { idx in
HStack {
Text(filteredTasks[idx].phase)
.font(.caption)
Spacer()
Text(filteredTasks[idx].name)
Spacer()
Toggle("", isOn: self.$filteredTasks[idx].isComplete)
}
}
}
This took quite a while for me to get to this piece of code, and I found that I had to follow an example with the 'indices' option to get the toggle to work on each Task individually, and to make sure that its isComplete value was saved.
Next, I wanted to filter the list of Tasks based on a Task variable, phase, which has values of Planning, Construction, or Final. So I created 4 buttons (one for each phase, and then an 'All Tasks' to get back to the full, unfiltered list), and after a lot of trial and error (creating filtered arrays that no longer were bound correctly, etc., etc.) I tried this, basically working only with the original array.
List {
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) }).indices, id: \.self) { idx in
HStack {
Text(project.tasks[idx].phase)
.font(.caption)
Spacer()
Text(project.tasks[idx].name)
Spacer()
Toggle("", isOn: self.$project.tasks[idx].isComplete)
}
}
}
And of course, this seemed to work because I can do a test:
func CreateTestArray() {
let testFilterArray = project.tasks.filter({ $0.phase.contains(filterValue) })
}
And that will give me the filtered list I want. However, in my ForEach view, it's not working correctly and I'm not sure how to work around it.
For example, I have 128 tasks, 10 of which have a value of 'Final' and when I use a button setting the filterValue to Final, the testFilterArray actually contains the correct 10 tasks - but in the ForEach view I'm getting the first ten tasks in the original array (which are of the type 'Planning' - the original array is sorted by Planning/Construction/Final); obviously the ForEach, in spite of the filter statement, is working on the original array. The Planning button sends the filterValue = "Planning", and I get the correct results because the filter returns 0-19 indices for the 20 Planning tasks I have in the original array, and since they're first in the original array, it 'appears' that the Planning filter is working correctly, tho in actually it's just by chance that it works, if the array were sorted differently it would not.
Any ideas how I can approach this so that I can actually filter on this array, display the isComplete toggle correctly for each item in the array, as well as update the toggle state dynamically? I feel like I need to start from scratch once again here because I've let these constraints work me into a tiny Swift corner.
Thanks!
Update:
Thank you, #jnpdx, for your quick response - and I should definitely have included objects (which I list below). However, looking back over my object definitions, I wonder if I've made an even more basic error in managing the objects which is why I've gotten boxed in to the situation I have (i.e., in earlier iterations I'd attempted some of your suggestion). At any rate, my published object is 'projects', which is a list of projects I pass to the project list view, and then that view passes a single project to a project view, and then that view lists the tasks in that particular project.
I feel like your answer is pointing me in the right decision, I just need to back back up and look at those object definitions/management and see how to get to a situation where a straightforward solution is possible.
Task:
struct Task: Identifiable, Codable {
let id: UUID
var phase: String
var category: String
var name: String
var isComplete: Bool
init(id: UUID = UUID(), phase: String, category: String, name: String, isComplete: Bool) {
self.id = id
self.phase = phase
self.category = category
self.name = name
self.isComplete = isComplete
}
}
The Project:
struct Project: Identifiable, Codable {
var id: UUID
var name: String
var type: String
var tasks: [Task]
var isComplete: Bool
init(id: UUID = UUID(), name: String, type: String, tasks: [Task] = [], isComplete: Bool) {
self.id = id
self.name = name
self.type = type
self.tasks = tasks
self.isComplete = isComplete
}
}
and the Project model:
class ProjectData: ObservableObject {
// code to access the json file is here
// An accessible list of projects from the saved file
#Published var projects: [Project] = []
// load and save functions follow
Update:
Thanks, #jnpdx, your solution worked after making, as you said I would need to, the tweaks to get it to function within my particular model design. Here are the snippets that finally worked in my case.
In my view:
List {
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) })) { task in
HStack {
Text(task.name)
Toggle("", isOn: self.makeBinding(item: task))
}
}
}
And the called function:
func makeBinding(item: Task) -> Binding<Bool> {
let i = self.project.tasks.firstIndex { $0.id == item.id }!
return .init(
get: { self.project.tasks[i].isComplete },
set: { self.project.tasks[i].isComplete = $0 }
)
}
Let's look at the following line from your code:
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) }).indices, id: \.self) { idx in
In the first part, you filter tasks and then ask for the indices. My suspicion is that you're hoping it would return something like [1, 5, 10, 11, 12], meaning their original positions in the array. But, in reality, you're going to get a contiguous array like [0,1,2,3,4] because it's giving you indices from the newly-created array (the result of filter).
There are a couple of ways to solve this, which also relate to the previous ForEach that you had.
It's more idiomatic to do ForEach and iterate over structs/objects rather than indices. You don't show what Task is made up of, but let's say it's this:
struct Task : Hashable {
var id = UUID()
var name: String
var phrase: String
var isComplete: Bool
}
To iterate on it, you could do:
ForEach(task, id: \.id) { task in
Text(task.name)
Toggle("Done?", isOn: project.taskCompletedBinding(id: task.id)) //explained later
}
I asked about the type of Project in my comment because I'm not totally clear why it's a #Binding. It seems like maybe it's an object? If it's a view model, which would be nice, you can handle your Toggle logic there. Something like:
class Project : ObservableObject {
#Published var tasks : [Task] = [Task(name: "1", phrase: "phase", isComplete: false),Task(name: "2", phrase: "phase", isComplete: true),Task(name: "3", phrase: "phase2", isComplete: false)]
var completedTasks : [Task] {
return tasks.filter { $0.isComplete }
}
func taskCompletedBinding(id: UUID) -> Binding<Bool> {
Binding<Bool>(get: {
self.tasks.first(where: { $0.id == id})?.isComplete ?? false
}, set: { newValue in
self.tasks = self.tasks.map { t in
if t.id == id {
var tCopy = t
tCopy.isComplete = newValue
return tCopy
} else {
return t
}
}
})
}
}
And you can test that it works doing this:
struct ContentView: View {
#ObservedObject var project = Project()
var body: some View {
ForEach(project.tasks, id: \.id) { task in
Text(task.name)
Toggle("Done?", isOn: project.taskCompletedBinding(id: task.id))
}
}
}
If Project is a struct and not an object, it might be good to wrap it in an ObservableObject view model like I did above.
I'm try to speed up my APP that for now unfortunately is very slow on performing some search and list of data with Swift Ui.
first of all I have a data model that describe my object airport, called AirportModel
import Foundation
class AirportModel : Identifiable , Codable{
var aptICAO : String
var aptName : String
var aptCity : String
var aptCountry :String
var aptIATA : String
init(aptICAO: String, aptName: String, aptCity: String, aptCountry: String, aptIATA: String) {
self.aptICAO = aptICAO
self.aptName = aptName
self.aptCity = aptCity
self.aptCountry = aptCountry
self.aptIATA = aptIATA
}
}
I have a local file apt.json, that contain all the data information for my airport (I downloaded from internet this json, inside there are around 29000 airport)
so when I run the first time the app, with the following function , i create and save the airportVector of type AirportModel.
func openfilejson (fileName : String) {
if let path = Bundle.main.path(forResource: fileName, ofType: "json") {
do {
let fileUrl = URL(fileURLWithPath: path)
let datafile = try Data(contentsOf: fileUrl,options: .mappedIfSafe)
let json = JSON(data:datafile)
objectWillChange.send()
for (key,_) in json {
let airport = AirportModel(aptICAO: "", aptName: "", aptCity: "", aptCountry: "", aptIATA: "")
airport.aptName = json[key]["name"].stringValue
airport.aptCity = json[key]["city"].stringValue
airport.aptCountry = json[key]["country"].stringValue
airport.aptIATA = json[key]["iata"].stringValue
airport.aptICAO = json[key].stringValue
airportVector.append(airport)
}
debugPrint("nel vettore airport Vector ci sono \(airportVector.count) aeroporti")
save2() // to save airport vector in the plistfile
debugPrint("SALVATO IN MEMORIA ")
} catch {
print("ERRORE OPEN FILE AEROPORTI")
}
}
}
all, this work fine , the vector with 29000 airport inside is created once the app is ru the first time.
NOW THE BIG ISSUE.
in one view, I try to list this airport using SwiftUI and a searchBar to search on it.
the problem is, due to the big amount of data to be opened, when I load the View with the list; the data take very long to be listed and moreover when I lunch the search everything is stuck!
any idea how can I process or speed up this huge amount of data to avoid the app stuck on the loading of the following view!
import SwiftUI
struct ContentView: View {
#ObservedObject var dm: DataManager
#State private var searchTerm : String = ""
var body: some View {
VStack {
Text("List")
SearchBar(text: $searchTerm).shadow(radius: 10)
List(dm.airportVector.filter{
$0.aptCity.localizedCaseInsensitiveContains(searchTerm)
|| $0.aptName.localizedCaseInsensitiveContains(searchTerm)
}){ item in
HStack {
Text(item.aptICAO).bold().font(Font.system(size:20)).frame(width: 90, height: 10)
//
Text(item.aptName).font(Font.system(size: 15))
Spacer()
VStack(alignment: .leading) {
Text(item.aptCity).font(Font.system(size: 10)).font(.subheadline)
Spacer()
Text(item.aptCountry).font(Font.system(size: 10))
}
}
}
}
}
}
Thanks in advance.
Damiano
Make your data conform to Identifiable and use ForEach inside of your List with a separate View to render the item:
List {
ForEach(items) {
MyItem(item)
}
}
Only render items when they change by making MyItem conform to Equatable, and define the == function. Make sure the == function is being executed by adding a breakpoint or print statement. If it's not being executed see this article for a workaround:
https://swiftui-lab.com/equatableview/
If the above suggestion works, but you find yourself making wholesale modifications to the list after initial render, try using id() during these modifications. See https://swiftui-lab.com/swiftui-id/
If the above suggestions don't improve things enough, you probably need a more custom solution. Use a TableView or CollectionView, see https://github.com/apptekstudios/ASCollectionView
Hi guys I don't know why the array Places returns weird values like 0x6080004b3aa0 instead of displaying my title, coordinate and subtitle out of my JSON url. Thanks for your Help!
import MapKit
#objc class Place: NSObject {
var title: String?
var coordinate: CLLocationCoordinate2D
var subtitle: String?
init(title:String,subtitle:String, coordinate:CLLocationCoordinate2D){
self.title = title
self.coordinate = coordinate
self.subtitle = subtitle
}
static func getPlaces() -> [Place] {
guard let url = NSURL(string: "https://script.googleusercontent.com/macros/echo?user_content_key=Z-LfTMdhgAg_6SRd-iMucSyWu-LFBQO8MLxJZ6DPcL05Rtr3joCCypWD2l46qaegSpVpVINc1DLl5inoDOgGx3p3ANpY1AkGOJmA1Yb3SEsKFZqtv3DaNYcMrmhZHmUMWojr9NvTBuBLhyHCd5hHa1ZsYSbt7G4nMhEEDL32U4DxjO7V7yvmJPXJTBuCiTGh3rUPjpYM_V0PJJG7TIaKp4bydEiKBUZP6fpOyGJIhkmEGneM7ZIlWloTVbXmkjs15vHn8T7HCelqi-5f3gf3-sKiW3k6MDkf31SIMZH6H4k&lib=MbpKbbfePtAVndrs259dhPT7ROjQYJ8yx") else { return [] }
let request = NSMutableURLRequest(url: url as URL!)
var places = [Place]()
let task = URLSession.shared.dataTask(with: request as URLRequest) {data,response,error in
guard error == nil && data != nil else {
print ("Error:",error)
return
}
let httpStatus = response as? HTTPURLResponse
if httpStatus?.statusCode == 200
{ if data?.count != 0
{
let responseString = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! NSDictionary
let contacts = responseString["Sheet1"] as? [AnyObject]
for contact in contacts!{
var places = [Place]()
let title = contact["name"] as! String
let subtitle = contact["description"] as? String
let latitude = contact["latitude"] as? Double ?? 0, longitude = contact["longitude"] as? Double ?? 0
let place = Place(title:title,subtitle:subtitle!,coordinate: CLLocationCoordinate2DMake(latitude, longitude))
places.append(place)
print(latitude)
print(place)
}
}
else {
print("No data got from url")
}
} else {
print("error httpsatus code is:", httpStatus!.statusCode)
}
}
task.resume()
return places as [Place]
}
}
I think the problem is this:
let place = Place(title:title,subtitle:subtitle!,coordinate: CLLocationCoordinate2DMake(latitude, longitude))
When I print(place) it returns the weird results
When you make a class that subclasses from NSObject you're creating a object that is backed by an Objective-c class -- which in some circumstances can be really useful (most common use is when you want to take your object and archive it as a blob of binary data).
I'm guessing that in your case, you probably don't want/need to subclass NSObject.
Here's a simplified example to show what's happening:
Here's a class backed by NSObject:
#objc class ObjCPlace: NSObject {
let name: String
init(name: String) {
self.name = name
}
}
If you create an instance of this object and try to print contents - like you've found, you get the objects location in memory:
let testObjcPlace = ObjCPlace(name: "New York")
print(testObjcPlace)
// prints:
// <__lldb_expr_310.ObjCPlace: 0x600000055060>
On alternative to using print could be to use dump that provides a more detailed look at your object:
let testObjcPlace = ObjCPlace(name: "New York")
dump(testObjcPlace)
// Prints:
// ▿ <__lldb_expr_310.ObjCPlace: 0x600000055060> #0
// - super: NSObject
// - name: "New York"
But instead of making an NSObject subclass, you probably just want to make a Swift class (or in this example a struct take a look at this question and answers for explanations of the differences)
struct Place {
let name: String
init(name: String) {
self.name = name
}
}
Because this object isn't an Objective-c object you can use print and get the internal properties printed as well:
let testPlace = Place(name: "New York")
print(testPlace)
// Prints:
// Place(name: "New York")
p/s welcome to StackOverflow!