How to show Firebase images in SwiftUI using an ImageSlider or something similar - arrays

I have been trying to do this for some time, but have not been able to achieve my goal. I would like to show images stored at Firebase in an Image Slider or carousel in SwiftUI. I am able to fetch and show a single image (that's a different thing). However, unable to load images from firebase to slider. The first part is to get url of images which I am not getting how to do it.
Update
This loads images but cant auto scroll or auto iterate, images load one after another than stops:
func downImage(){
let imageRef = Storage.storage()
let loc = imageRef.reference().child("Images")
loc.listAll{(result,error) in
if error != nil{
print("Ooops : ", error)
return
}
for item in result!.items{
let count = result?.items.count ?? 5
//provides actual count ie 3
print("number of Images: ", self.count)
print("images url : " , item)
item.getData(maxSize: 15 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
guard let data = data else { return }
DispatchQueue.main.async {
self.count = count
// self.actualImage.append(self.image)
self.image = UIImage(data: data as Data)!
}}}}}}
In Slider, images are shown but stops after loading third image:
struct ImageSlider: View {
#StateObject var imageList = FireImageModel()
/* private let timer = Timer.publish(every: 3, on: .main, in: .common)
.autoconnect() */
var body: some View {
TabView(){
/* ForEach(imageList.actualImage, id: \.self) { item in
if foreach loop is used with actualImage appending Images no
image is shown or Index out of range error */
Image(uiImage: imageList.image)
.resizable()
.scaledToFill()
}
.tabViewStyle(PageTabViewStyle())
.animation(.easeInOut(duration: 3))
.onAppear(){
imageList.downImage()
}
}
If I apply the timer (commented out) with other settings it keeps updating selected image 0, selected image 1 but image never changes. Need images to auto scroll in this slider.

As I understand your code, you are downloading the string url from Firebase into
#Published var imageURL: [String] = []. This does not download the image data, just the url string.
In your loadImageURL, try this, to store all the url strings into your imageURL array:
DispatchQueue.main.async {
if let linq = link {
self.imageURL.append(linq)
print("url link : ", self.imageURL)
}
In your ImageSlider, use something like this, instead of your Image(item):
AsyncImage(url: URL(string: item))
also, change #ObservedObject var imageList = FireImageModel() to #StateObject var imageList = FireImageModel()
and, do not use !, anywhere in your code, it is a recipe for disaster.
EDIT-1:
Given your new code, try this simple example code, to try to find where your problem is:
struct ImageSlider: View {
#StateObject var imageList = FireImageModel()
var body: some View {
ScrollView {
ForEach(imageList.actualImage, id: \.self) { item in
Image(uiImage: item)
.resizable()
.frame(width: 222, height: 222)
}
.onAppear {
imageList.downImage()
}
}
}
}
Also, do not use ! anywhere in your code.
If that works, try this:
struct ImageSlider: View {
#StateObject var imageList = FireImageModel()
var body: some View {
VStack {
if imageList.actualImage.count > 0 {
TabView {
ForEach(imageList.actualImage, id: \.self) { item in
Image(uiImage: item)
.resizable()
.scaledToFill()
}
}
.tabViewStyle(PageTabViewStyle())
} else {
ProgressView()
}
}
.onAppear {
imageList.downImage()
}
}
}
You may also want to clean your FireImageModel, such as:
class FireImageModel: ObservableObject {
#Published var actualImage: [UIImage] = []
func downImage() {
let imageRef = Storage.storage()
let loc = imageRef.reference().child("Images")
loc.listAll{(result,error) in
if error != nil{
print("Ooops : ", error)
return
}
if let images = result {
print("number of Images: ", images.items.count)
for item in images.items {
print("images url : " , item)
item.getData(maxSize: 15 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
DispatchQueue.main.async {
if let theData = data, let image = UIImage(data: theData) {
self.actualImage.append(image)
}
}
}
}
}
}
}
}

I was able to display images in slider final prob was auto iteration. ForEach was throwing index out of range error(known prob) when directly using an array having image data so ans by #workingdogUkraine helped to resolve the error by putting check before entering the foreach. Auto iteration is done. Look into updated clean FireImageModel model by WorkingDog, below is the slider struct:
struct ImageSlider: View {
#StateObject var imageList = FireImageModel()
#State private var currentIndex = 0
#State var zoomed = false
private let timer = Timer.publish(every: 3, on: .main, in: .common)
.autoconnect()
var body: some View {
// 2
VStack {
if imageList.actualImage.count > 0 {
GeometryReader{ proxy in
TabView(selection: $currentIndex){
ForEach(imageList.actualImage.indices, id: \.self) {
item in
let sot = imageList.actualImage[item]
Image(uiImage: sot)
.resizable()
.scaledToFill()
.onTapGesture {
self.zoomed.toggle()
}
.scaleEffect(self.zoomed ? 1.73 : 1)
}
.onReceive(timer, perform: {_ in
withAnimation(.spring(response: 1,
dampingFraction: 1, blendDuration: 1)){
currentIndex = currentIndex <
imageList.actualImage.count ? currentIndex + 1 : 0
}
})
}
.tabViewStyle(PageTabViewStyle())
.frame(width: proxy.size.width, height: proxy.size.height/3)
}
}else {
ProgressView()
}
}.background(.ultraThinMaterial)
.onAppear {
imageList.downImage()
}
}}
Images appear with non noticeable animation :), will like to add some cool animation, after every 3 seconds image changes

Related

Create array of attributes values from fetch entity of CoreData [duplicate]

struct PickerView: View {
var items0 : [PageName]
#State var selectedPage: PageName?
init(items0: [PageName]) {
self.items0 = items0
self._selectedPage = State(initialValue: items0.first)
}
var body: some View {
Picker(selection: $selectedPage, label: Text("Page")) {
ForEach(items0) { item in
Text(item.pageName ?? "").tag(item as PageName?)
}
}
Text("\((selectedPage?.pageName)!)")
}
NavigationView {
Form {
PickerView(items0: Array(items0))
}
.foregroundColor(.blue)
.background(Color.yellow)
}
I can successfully pick the value of Core Data in Picker, but I don't know how can I use this value in my main View...
Some much things I already tried, but that Core Data drives me just crazy.
Can someone help me that I can use the picked value of the picker with Core Data in SwiftUI?
I misunderstood your issue with my original comment see below. There is a lot of info in the comments.
import SwiftUI
struct ParentPickerView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \PageName.pageName, ascending: true)],
animation: .default)
private var items0: FetchedResults<PageName>
//State is a source of truth
#State var selectedPage: PageName? = nil
var body: some View {
NavigationView {
Form {
Text(selectedPage?.pageName ?? "not selected")
PickerView(items0: Array(items0), selectedPage: $selectedPage)
}
.foregroundColor(.blue)
.background(Color.yellow)
}
}
}
struct PickerView: View {
//If you don't need all your PageName in your parentView it would be best to have the fetch here
// #Environment(\.managedObjectContext) private var viewContext
// #FetchRequest(
// sortDescriptors: [NSSortDescriptor(keyPath: \PageName.pageName, ascending: true)],
// animation: .default)
// private var items0: FetchedResults<PageName>
var items0: [PageName]
//Binding is a two-way connection
#Binding var selectedPage: PageName?
//You don't need custom init in SwiftUI because they are struct
// init(items0: [PageName], selectedPage: Binding<PageName?>) {
// self.items0 = items0
// self._selectedPage = selectedPage
//
// //#State Init here is bad practice as you are experiencing it becomes a dead end
// //self._selectedPage = State(initialValue: items0.first)
// }
var body: some View {
Picker(selection: $selectedPage, label: Text("Page")) {
ForEach(items0) { item in
Text(item.pageName ?? "").tag(item as PageName?)
}
}
Text("\((selectedPage?.pageName ?? "not selected"))")
.onAppear(){
//Set your initial item here only if the selected item is nil
if selectedPage == nil{
selectedPage = items0.first
}
}
}
}

Swift Struct in Array of Structs not updating to new values

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

Swift - Cannot append Object to Array

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]
}

Find if a different array has the same element and open an Alert

I know from the question it looks like something that has been already answered on this website before, but please read until the end, because I can't find the answer.
So, I have an Array that contains values of TagsModel:
import SwiftUI
import Combine
class DataManager : Equatable, Identifiable, ObservableObject {
static let shared = DataManager()
#Published var storageTags : [TagsModel] = []
typealias StorageTags = [TagsModel]
//The rest of the code
}
And the TagsModel:
import SwiftUI
import Combine
class TagsModel : Codable, Identifiable, Equatable, ObservableObject {
var id = UUID()
var tagName : String
var value : [ValueModel] = []
init(tagName: String) {
self.tagName = tagName
}
static func == (lhs: TagsModel, rhs: TagsModel) -> Bool {
return lhs.id.uuidString == rhs.id.uuidString
}
}
If you need it, the ValueModel is:
import SwiftUI
import Combine
class ValueModel : Codable, Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var name : String
var notes : String?
var expires : Date?
init(name: String, notes: String?, expires: Date?) {
self.name = name
self.notes = notes
self.expires = expires
}
}
Now what I would like to do is let the user add the elements of type ValueModel to the array Value of each TagsModel (which in English means I would like users to be able to add values inside their belonging tags). I can do all this, but I would like to add a check: if any other TagsModel contains the value that the user is trying to add, show an Alert (since every value can have only one tag). This Alert should be asking the user whether he/she wants to add that value to the selected tag and remove it from the other one, or cancel the action.
What I managed to do up to now is this:
import SwiftUI
struct SelectValuesForTagsView: View {
#ObservedObject var dm: DataManager
var tm: TagsModel
#Binding var showSheetSelectValuesForTagsView : Bool
#State var showAlertValueAlreadyInTag = false
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#GestureState private var dragOffset = CGSize.zero
#Environment(\.colorScheme) var colorScheme
var body: some View {
NavigationView {
Form {
ForEach(dm.storageValues) { valuesOfForEach in
HStack {
if tm.value.contains(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) {
Image(systemName: "checkmark.circle.fill")
.frame(width: 22, height: 22)
.foregroundColor(.green)
} else {
Image(systemName: "circle")
.frame(width: 22, height: 22)
.foregroundColor(colorScheme == .dark ? .white : .black)
}
Button(action: {
if !tm.value.contains(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) {
tm.value.append(valuesOfForEach)
dm.save()
} else {
guard let indexValue = tm.value.firstIndex(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) else { return }
tm.value.remove(at: indexValue)
dm.save()
}
}, label: {
if tm.value.contains(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) {
Text(valuesOfForEach.name)
.foregroundColor(.blue)
} else {
Text(valuesOfForEach.name)
.foregroundColor(.yellow)
}
})
}
}
}
.navigationBarTitle(Text("Add Values"), displayMode: .automatic)
.navigationBarBackButtonHidden(true)
.navigationBarItems(trailing: saveButton)
}
}
var saveButton: some View {
Button(action: {
let tag : TagsModel
if dm.storageTags.contains(where: {$0.value == tm.value}) {
showAlertValueAlreadyInTag = true
//The problem is here, that the Alert ALWAYS shows up, even though there's only this Tag containing that value
}
else {
dm.save()
showSheetSelectValuesForTagsView = false
}
}, label: {
Text("Save")
.foregroundColor(.blue)
}).alert(isPresented: $showAlertValueAlreadyInTag, content: { alertValueAlreadyInTag })
}
var alertValueAlreadyInTag : Alert {
Alert(title: Text("Attention!"), message: Text("Every value can only be assigned to one tag. One or more values you selected are already into another tag. Would you like to substitute the belonging tag for these values?"), primaryButton: Alert.Button.default(Text("Yes, substitute"), action: {
dm.save()
showAlertValueAlreadyInTag = false
showSheetSelectValuesForTagsView = false
}), secondaryButton: Alert.Button.default(Text("Cancel"), action: {
showAlertValueAlreadyInTag = false
}))
}
}
How can I check if another tag inside the storageTags has the already the same value inside of its value Array?

SwiftUI FetchRequest and onReceive cause infinite loop

I have a #FetchRequest for NSManagedObject in swiftUI. I’m trying to change the title of the first item in core data when the toggle is used, by calling onReceive. This does not work if the fetched results are used in MainVC. To demonstrate this, the Navigation Buttons title is how many elements are in the fetched result. This ends up creating an infinite loop in the ContentView onRecieve method. If the Navigation Button has regular text instead of using anything from the FetchedResults then there is no loop and everything works as expected. How is this loop being caused, and is there any better way of toggling a specific core data element?
import SwiftUI
import CoreData
final class ContentVCModel : ObservableObject{
#Published var newToDoItem = String()
#Published var shouldTurnOn = false
func createNewToDoItem(){
let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let toDoItem = ToDoItem(context: moc)
toDoItem.title = self.newToDoItem
toDoItem.createdAt = Date()
toDoItem.cost = Cost(main: "$\(Int.random(in: 1...99))", tax: "$\(Int.random(in: 1...9))")
toDoItem.isOn = false
ToDoItem.save()
self.newToDoItem = ""
}
}
struct ContentView: View {
#ObservedObject var model = ContentVCModel()
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: ToDoItem.getAllToDoItems()) var toDoItems : FetchedResults<ToDoItem>
var body: some View {
List{
Section(header : Text("Whats next?")){
Toggle("Toggle Test", isOn: $model.shouldTurnOn)
.onReceive(model.$shouldTurnOn) { (newValue) in
self.toDoItems.first?.title = "\(Int.random(in: 23...3423))"
//infitine loop
}
HStack{
TextField("New Item", text: $model.newToDoItem)
Button(action: {
self.model.createNewToDoItem()
}){
Image(systemName: "plus.circle.fill")
.foregroundColor(self.model.newToDoItem.isEmpty ? .gray : .green)
.imageScale(.large)
}.disabled(self.model.newToDoItem.isEmpty)
}
}.font(.headline)
Section(header: Text("To Do's")){
ForEach(toDoItems) { toDoItem in
ToDoItemView(title: toDoItem.title, createdAt: toDoItem.createdAt.description, cost: toDoItem.cost, isOn: true)
}.onDelete { (indexSet) in
let deleteItem = self.toDoItems[indexSet.first!]
self.managedObjectContext.delete(deleteItem)
ToDoItem.save()
}
}
}
.navigationBarTitle(Text("My List"))
.navigationBarItems(trailing: EditButton())
}
}
struct MainVC : View {
#FetchRequest(fetchRequest: ToDoItem.getAllToDoItems()) var toDoItems : FetchedResults<ToDoItem>
var body: some View {
NavigationView{
NavigationLink(destination: ContentView()) {
Text("\(toDoItems.count)") //Using toDoItems causes this to repeat ex: "\(toDoItems.count)"
}
}
}
}

Resources