Not possible to loop over PHFetchResult with ForEach - swift

I'm currently working on a photos app for iOS / macOS and i'm struggeling with PhotoKit.
I did create a class where i manage all my PhotoKit requests.
class PhotosAPI: ObservableObject {
#Published var all = PHFetchResult<PHAsset>()
#Published var allAlbums = PHFetchResult<PHAssetCollection>()
#Published var allSmartAlbums = PHFetchResult<PHAssetCollection>()
// Functions to get the Collections / Assets
}
This part is working so far but now i'm struggeling with showing those data in my View.
In my View i would like to present all Assets in a List / Grid
struct ShowImages: View {
#ObservedObject var photos = PhotosAPI()
var body: some View {
List(photos.all, id: \.self) { item in
Text("\(item)")
}
}
}
But i do get an error "Initializer 'init(_:id:rowContent:)' requires that 'PHFetchResult' conform to 'RandomAccessCollection'" and i did try all day today to fix this but i wasn't successful and i couldn't find anything useful in google.
Does anyone have an idea how i can get PHFetchResults to loop over them?
At the end i was able to show the pictures with below code. But this looks like very bad code to me. I would prefer to loop directly over the PHFetchResult. Does anyone know how i can get this done?
ForEach(0..<photos.all.count) { index in
Text("\(photos.all.object(at: index).localIdentifier)")
}

You can implement RandomAccessCollection for PHFetchResult or create wrapper that conforms RandomAccessCollection.
struct PHFetchResultCollection: RandomAccessCollection, Equatable {
typealias Element = PHAsset
typealias Index = Int
let fetchResult: PHFetchResult<PHAsset>
var endIndex: Int { fetchResult.count }
var startIndex: Int { 0 }
subscript(position: Int) -> PHAsset {
fetchResult.object(at: fetchResult.count - position - 1)
}
}
Then you will be able to use PHFetchResultCollection with ForEach
let collection = PHFetchResultCollection(fetchResult: fetchResult)
var body: some View {
ForEach(collection, id: \.localIdentifier) {
...
}
}

You can enumerate the items of the fetched results object with the enumerate methods, such as this one:
https://developer.apple.com/documentation/photokit/phfetchresult/1620999-enumerateobjects

Related

#AppStorage not updating view

I have the following reproduced example in Swift/SwiftUI.
Intended Functionality: List of numbers, where upon deletion of any number, the numbers update and reorganize so that they are in consecutive order.
Example: Suppose we have numbers 1, 2, 3, 4 in a list. When we delete number 2, the list should become 1, 2, 3 instead of staying like 1, 3, 4.
Problem: When #State is used to hold the array of numbers, the code works as expected. However, when an array in #AppStorage is used to hold the array, the view does not seem to update/change the numbers.
So, why can't I use the #AppStorage approach, and how can I fix this?
I know the code looks like a lot, but you can mostly ignore it if you just read the comments. It's just a body with a couple of functions, nothing crazy.
class MyObj: Codable, Identifiable { //SIMPLE OBJECT TO HOLD A NUMBER
init(num: Int) {
self.num = num
}
var id: UUID = UUID()
var num: Int
}
struct test2: View {
#State var array: [MyObj] = [] //USING THIS (#State) WORKS
//AppStorage("array") var array: [MyObj] = [] //USING THIS (#AppStorage) DOESN’T UPDATE NUMBERS
func minimizeNums() {
for i in 0..<array.count { //MAKES NUMBERS IN ORDER/CONSECUTIVE
array[i].num = i
}
}
var body: some View {
VStack {
Button("add number object") {
array.append(MyObj(num: array.count))
}
List {
ForEach(array) { obj in
Text(String(obj.num))
}
.onDelete(perform: { index in
array.remove(atOffsets: index) //REMOVES NUMBER OBJECT FROM LIST
minimizeNums() //MAKES THE REMAINING OBJECT'S NUMBERS CONSECUTIVE
})
}
}
}
}
Important: I used the extension from this accepted answer in order to store arrays in #AppStorage. I assume this extension may be contributing to the problem, but I'm not sure how!
This is failing, because you are using a class for your model MyObj. There are multiple reasons for using structs with SwiftUI. Please read Blog entry or any other tutorial or documentation.
[TLDR]:
Don´t use classes use structs.
Changing MyObj to:
struct MyObj: Codable, Identifiable {
init(num: Int) {
self.num = num
}
var id: UUID = UUID()
var num: Int
}
should work.

Swift & SwiftUI - Conditional global var

I want to make a global variable in Swift, so that its Data is accessible to any view that needs it. Eventually it will be a var so that I can mutate it, but while trying to get past this hurdle I'm just using it as let
I can do that by putting this as the top of a file (seemingly any file, Swift is weird):
let myData: [MyStruct] = load("myDataFile.json)
load() returns a JSONDecoder(). MyStruct is a :Hashable, Codable, Identifiable struct
That data is then available to any view that wants it, which is great. However, I want to be able to specify the file that is loaded based on a condition - I'm open to suggestions, but I've been using an #AppStorage variable to determine things when inside a View.
What I'd like to do, but can't, is do something like:
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
if(appStorageVar == "Condition2") {
let myData: [MyStruct] = load("myDataFile2.json")
}
else {
let myData: [MyStruct] = load("myDataFile.json")
}
I can do this inside a View's body, but then it's only accessible to that View and then I have to repeat it constantly, which can't possibly the correct way to do it.
You could change just change the global in an onChange on the AppStorage variable. This is an answer to your question, but you have the problem that no view is going to be updating itself when the global changes.
var myData: [MyStruct] = load("myDataFile.json)
struct ContentView: View {
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
var body: some View {
Button("Change value") {
appStorageVar = "Condition2"
}
.onChange(of: appStorageVar) { newValue in
myData = load(newValue == "Condition1" ? "myDataFile.json" : "myDataFile2.json")
}
}
}

Is it possible to instantiate a SwiftUI view from a string?

Is it possible to convert a string to a SwiftUI view? Something like JavaScript's eval(). In the code below I'm looking for an XXX function.
let uiString: String = “VStack { Text(\“hi there\”) }”
let view: some View = XXX(uiString)
!!! Not recommended !!! Do not try This at home!
Here is an example, but you need to consider any possible view, or may you just limit your work for just some special input, like I did.
PS: You can do some big optimization in codes to make it better and faster with using some more for loop, switch or enums, but at the end of the day, the things that I showed here is work of SwiftUI not us! And not recommended. It was just a show case that how code be done.
struct ContentView: View {
var body: some View {
StringDecoderView(string: "Circle().fill(Color.red); Rectangle()")
}
}
struct StringDecoderView: View {
let string: String
var body: some View {
let arrayOfComponents: [String] = string.components(separatedBy: ";")
var anyView: [CustomViewType] = [CustomViewType]()
for item in arrayOfComponents {
if item.contains("Circle()") {
if item.contains(".fill") && item.contains("Color.red") {
anyView.append(CustomViewType(anyView: AnyView(Circle().fill(Color.red))))
}
else {
anyView.append(CustomViewType(anyView: AnyView(Circle())))
}
}
else if item.contains("Rectangle()") {
anyView.append(CustomViewType(anyView: AnyView(Rectangle())))
}
}
return ForEach(anyView) { item in
item.anyView
}
.padding()
}
}
struct CustomViewType: Identifiable {
let id: UUID = UUID()
var anyView: AnyView
}
No, because Swift is compiled to machine code. There isn't an interpreter to evaluate arbitrary expressions on the go.
That sounds a lot like a security vulnerability. Why would you want to do that?

ForEach not properly updating with dynamic content SwiftUI

Sorry to make this post so long, but in hindsight I should have shown you the simpler instance of the issue so you could better understand what the problem is. I am assuming the same issue with ForEach is at the root cause of both of these bugs, but I could be wrong. The second instance is still included to give you context, but the first intance should be all you need to fully understand the issue.
First Instance:
Here is a video of the issue: https://imgur.com/a/EIg9TSm. As you can see, there are 4 Time Codes, 2 of which are favorite and 2 are not favorites (shown by the yellow star). Additionally, there is text at the top that represents the array of Time Codes being displayed just as a list of favorite (F) or not favorite (N). I click on the last Time Code (Changing to favorite) and press the toggle to unfavorite it. When I hit save, the array of Time Codes is updated, yet as you see, this is not represented in the List. However, you see that the Text of the reduced array immediately updates to FNFF, showing that it is properly updated as a favorite by the ObservedObject.
When I click back on the navigation and back to the page, the UI is properly updated and there are 3 yellow stars. This makes me assume that the problem is with ForEach, as the Text() shows the array is updated but the ForEach does not. Presumably, clicking out of the page reloads the ForEach, which is why it updates after exiting the page. EditCodeView() handles the saving of the TimeCodeVieModel in CoreData, and I am 99% certain that it works properly through my own testing and the fact that the ObservedObject updates as expected. I am pretty sure I am using the dynamic version of ForEach (since TimeCodeViewModel is Identifiable), so I don't know how to make the behavior update immediately after saving. Any help would be appreciated.
Here is the code for the view:
struct ListTimeCodeView: View {
#ObservedObject var timeCodeListVM: TimeCodeListViewModel
#State var presentEditTimeCode: Bool = false
#State var timeCodeEdit: TimeCodeViewModel?
init() {
self.timeCodeListVM = TimeCodeListViewModel()
}
var body: some View {
VStack {
HStack {
Text("TimeCodes Reduced by Favorite:")
Text("\(self.timeCodeListVM.timeCodes.reduce(into: "") {$0 += $1.isFavorite ? "F" : "N"})")
}
List {
ForEach(self.timeCodeListVM.timeCodes) { timeCode in
TimeCodeDetailsCell(fullName: timeCode.fullName, abbreviation: timeCode.abbreviation, color: timeCode.color, isFavorite: timeCode.isFavorite, presentEditTimeCode: $presentEditTimeCode)
.contentShape(Rectangle())
.onTapGesture {
timeCodeEdit = timeCode
}
.sheet(item: $timeCodeEdit, onDismiss: didDismiss) { detail in
EditCodeView(timeCodeEdit: detail)
}
}
}
}
}
}
Here is the code for the View Models (shouldn't be relevant to the problem, but included for understanding):
class TimeCodeListViewModel: ObservableObject {
#Published var timeCodes = [TimeCodeViewModel]()
init() {
fetchAllTimeCodes()
}
func fetchAllTimeCodes() {
self.timeCodes = CoreDataManager.shared.getAllTimeCodes().map(TimeCodeViewModel.init)
}
}
class TimeCodeViewModel: Identifiable {
var id: String = ""
var fullName = ""
var abbreviation = ""
var color = ""
var isFavorite = false
var tags = ""
init(timeCode: TimeCode) {
self.id = timeCode.id!.uuidString
self.fullName = timeCode.fullName!
self.abbreviation = timeCode.abbreviation!
self.color = timeCode.color!
self.isFavorite = timeCode.isFavorite
self.tags = timeCode.tags!
}
}
Second Instance:
EDIT: I realize it may be difficult to understand what the code is doing, so I have included a gif demoing the problem (unfortunately I am not high enough reputation for it to be shown automatically). As you can see, I select the cells I want to change, then press the button to assign that TimeCode to it. The array of TimeCodeCellViewModels changes in the background, but you don't actually see that change until I press the home button and then reopen the app, which triggers a refresh of ForEach. Gif of issue. There is also this video if the GIF is too fast: https://imgur.com/a/Y5xtLJ3
I am trying to display a grid view using a VStack of HStacks, and am running into an issue where the ForEach I am using to display the content is not refreshing when the array being passed in changes. I know the array itself is changing because if I reduce it to a string and display the contents with Text(), it properly updates as soon as a change is made. But, the ForEach loop only updates if I close and reopen the app, forcing the ForEach to reload. I know that there is a special version of ForEach that is specifically designed for dynamic content, but I am pretty sure I am using this version since I pass in '''id: .self'''. Here is the main code snippet:
var hoursTimeCode: [[TimeCodeCellViewModel]] = []
// initialize hoursTimeCode
VStack(spacing: 3) {
ForEach(self.hoursTimeCode, id: \.self) {row in
HStack(spacing: 3){
HourTimeCodeCell(date: row[0].date) // cell view for hour
.frame(minWidth: 50)
ForEach(row.indices, id: \.self) {cell in
// TimeCodeBlockCell displays minutes normally. If it is selected, and a button is pressed, it is assigned a TimeCode which it will then display
TimeCodeBlockCell(timeCodeCellVM: row[cell], selectedArray: $selectedTimeCodeCells)
.frame(maxWidth: .infinity)
.aspectRatio(1.0, contentMode: .fill)
}
}
}
}
I'm pretty sure it doesn't change anything, but I did have to define a custom hash function for the TimeCodeCellViewModel, which might change the behavior of the ForEach (the attributes being changed are included in the hash function). However, I have noticed the same ForEach behavior in another part of my project that uses a different view model, so I highly doubt this is the issue.
class TimeCodeCellViewModel:Identifiable, Hashable {
static func == (lhs: TimeCodeCellViewModel, rhs: TimeCodeCellViewModel) -> Bool {
if lhs.id == rhs.id {
return true
}
else {
return false
}
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(isSet)
hasher.combine(timeCode)
hasher.combine(date)
}
var id: String = ""
var date = Date()
var isSet = false
var timeCode: TimeCode
var frame: CGRect = .zero
init(timeCodeCell: TimeCodeCell) {
self.id = timeCodeCell.id!.uuidString
self.date = timeCodeCell.date!
self.isSet = timeCodeCell.isSet
self.timeCode = timeCodeCell.toTimeCode!
}
}
Here is a snippet of what you need to make the code work.
See the comments for some basics of why
struct EditCodeView:View{
#EnvironmentObject var timeCodeListVM: TimeCodeListViewModel
//This will observe changes to the view model
#ObservedObject var timeCodeViewModel: TimeCodeViewModel
var body: some View{
EditTimeCodeView(timeCode: timeCodeViewModel.timeCode)
.onDisappear(perform: {
//*********TO SEE CHANGES WHEN YOU EDIT
//uncomment this line***********
//_ = timeCodeListVM.update(timeCodeVM: timeCodeViewModel)
})
}
}
struct EditTimeCodeView: View{
//This will observe changes to the core data entity
#ObservedObject var timeCode: TimeCode
var body: some View{
Form{
TextField("name", text: $timeCode.fullName.bound)
TextField("appreviation", text: $timeCode.abbreviation.bound)
Toggle("favorite", isOn: $timeCode.isFavorite)
}
}
}
class TimeCodeListViewModel: ObservableObject {
//Replacing this whole thing with a #FetchRequest would be way more efficient than these extra view models
//IF you dont want to use #FetchRequest the only other way to observe the persistent store for changes is with NSFetchedResultsController
//https://stackoverflow.com/questions/67526427/swift-fetchrequest-custom-sorting-function/67527134#67527134
//This array will not see changes to the variables of the ObservableObjects
#Published var timeCodeVMs = [TimeCodeViewModel]()
private var persistenceManager = TimeCodePersistenceManager()
init() {
fetchAllTimeCodes()
}
func fetchAllTimeCodes() {
//This method does not observe for new and or deleted timecodes. It is a one time thing
self.timeCodeVMs = persistenceManager.retrieveObjects(sortDescriptors: nil, predicate: nil).map({
//Pass the whole object there isnt a point to just passing the variables
//But the way you had it broke the connection
TimeCodeViewModel(timeCode: $0)
})
}
func addNew() -> TimeCodeViewModel{
let item = TimeCodeViewModel(timeCode: persistenceManager.addSample())
timeCodeVMs.append(item)
//will refresh view because there is a change in count
return item
}
///Call this to save changes
func update(timeCodeVM: TimeCodeViewModel) -> Bool{
let result = persistenceManager.updateObject(object: timeCodeVM.timeCode)
//You have to call this to see changes at the list level
objectWillChange.send()
return result
}
}
//DO you have special code that you aren't including? If not what is the point of this view model?
class TimeCodeViewModel: Identifiable, ObservableObject {
//Simplify this
//This is a CoreData object therefore an ObservableObject it needs an #ObservedObject in a View so changes can be seem
#Published var timeCode: TimeCode
init(timeCode: TimeCode) {
self.timeCode = timeCode
}
}
Your first ForEach probably cannot check if the identity of Array<TimeCodeCellViewModel> has changed.
Perhaps you want to use a separate struct which holds internally an array of TimeCodeCellViewModel and conforms to Identifiable, effectively implementing such protocol.
stuct TCCViewModels: Identifiable {
let models: Array<TimeCodeCellViewModel>
var id: Int {
models.hashValue
}
}
You might as well make this generic too, so it can be reused for different view models in your app:
struct ViewModelsContainer<V: Identifiable> where V.ID: Hashable {
let viewModels: Array<V>
let id: Int
init(viewModels: Array<V>) {
self.viewModels = viewModels
var hasher = Hasher()
hasher.combine(viewModels.count)
viewModels.forEach { hasher.combine($0.id) }
self.id = hasher.finalize
}
}

SwiftUI ForEach Binding compile time error looks like not for-each

I'm starting with SwiftUI and following WWDC videos I'm starting with #State and #Binding between two views. I got a display right, but don't get how to make back-forth read-write what was not include in WWDC videos.
I have model classes:
class Manufacturer {
let name: String
var models: [Model] = []
init(name: String, models: [Model]) {
self.name = name
self.models = models
}
}
class Model: Identifiable {
var name: String = ""
init(name: String) {
self.name = name
}
}
Then I have a drawing code to display that work as expected:
var body: some View {
VStack {
ForEach(manufacturer.models) { model in
Text(model.name).padding()
}
}.padding()
}
and I see this:
Canvas preview picture
But now I want to modify my code to allows editing this models displayed and save it to my model #Binding so I've change view to:
var body: some View {
VStack {
ForEach(self.$manufacturer.models) { item in
Text(item.name)
}
}.padding()
}
But getting and error in ForEach line:
Generic parameter 'ID' could not be inferred
What ID parameter? I'm clueless here... I thought Identifiable acting as identifier here.
My question is then:
I have one view (ContentView) that "holds" my datasource as #State variable. Then I'm passing this as #Binding to my ManufacturerView want to edit this in List with ForEach fill but cannot get for each binding working - how can I do that?
First, I'm assuming you have something like:
#ObservedObject var manufacturer: Manufacturer
otherwise you wouldn't have self.$manufacturer to begin with (which also requires Manufacturer to conform to ObservableObject).
self.$manufacturer.models is a type of Binding<[Model]>, and as such it's not a RandomAccessCollection, like self.manufacturer.models, which is one of the overloads that ForEach.init accepts.
And if you use ForEach(self.manufacturer.models) { item in ... }, then item isn't going to be a binding, which is what you'd need for, say, a TextField.
A way around that is to iterate over indices, and then bind to $manufacturer.models[index].name:
ForEach(manufacturer.indices) { index in
TextField("model name", self.$manufacturer.models[index].name)
}
In addition to that, I'd suggest you make Model (and possibly even Manufacturer) a value-type, since it appears to be just a storage of data:
struct Model: Identifiable {
var id: UUID = .init()
var name: String = ""
}
This isn't going to help with this problem, but it will eliminate possible issues with values not updating, since SwiftUI wouldn't detect a change.