AudioPlayerView is not being updated when the func checkIndex() is being triggered - ios

My AudioPlayerView(audioSelected:, audioTitle:)
is not being updated when the func checkIndex() is being triggered.
I'm trying unsuccessfully to update the audio file when using the checkIndex() function.
Not finding a way to update the AudioPlayerView() accordingly. Everything else works and gets updated on each view.
Get a warning Result of 'AudioPlayerView' initializer is unused
Please have a look at my code. Much appreciate any help. I'm new to swift. I do have a zip of project on GitHub if need more information. Thank you.
AudioPlayerView(audioSelected: vm.animals[animalID -
1].audio, audioTitle: "")
Button {
checkIndex()
} label: {
HStack {
Image(systemName:
"arrowshape.turn.up.right")
if animalID == 5 {
Text("Go to \
(vm.animals[0].animalName)")
} else {
Text("Go to \
(vm.animals[animalID].animalName)")
}
}//HSTACK
}//LABEL
// MARK: - Function
func checkIndex() {
let numAnimals = vm.animals.count
if animalID == numAnimals {
animalID = 1
} else {
animalID += 1
}//ELSE
//if AudioPlayerView(audioSelected: "", audioTitle:
"").self == 1 {
//vm.animals[animalID - 1].audio += 1
//}
//if animalID == 1 {
//AudioPlayerView(audioSelected: vm.animals[animalID -
//2].audio, audioTitle: "")
//}
}//INDEX
//full code
import SwiftUI
import Foundation
struct DetailView: View {
// MARK: - Properties
#State var animalID: DataModel.ID
#ObservedObject var vm: DetailViewModel
// MARK: - Body
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .center, spacing: 10){
// View will be updated when #State changes
// In here, the view is just a Text but you can put more complex
views...
Image(vm.animals[animalID - 1].image)
.resizable()
.scaledToFit()
.cornerRadius(0)
Text(vm.animals[animalID - 1].animalName)
.font(.largeTitle)
.padding()
//AUDIO
AudioPlayerView(audioSelected: vm.animals[animalID -
1].audio, audioTitle: "")
//END AUDIO
Text(verbatim: vm.animals[animalID -
1].description)
.multilineTextAlignment(.leading)
.layoutPriority(1)
.padding()
Spacer()
Button {
checkIndex()
} label: {
HStack {
Image(systemName:
"arrowshape.turn.up.right")
if animalID == 5 {
Text("Go to \
(vm.animals[0].animalName)")
} else {
Text("Go to \
(vm.animals[animalID].animalName)")
}
}//HSTACK
}//LABEL
.padding(.all, 10)
.background(Color(.lightGray))
.clipShape(RoundedRectangle(cornerRadius: 20))
//Spacer()
}//VStack
}//SCROLL
}//VIEW
//}//END VIEW
// MARK: - Function
func checkIndex() {
let numAnimals = vm.animals.count
if animalID == numAnimals {
animalID = 1
} else {
animalID += 1
}//ELSE
//This is what I've tried but did not work
//if AudioPlayerView(audioSelected: "", audioTitle:
//"").self == 1 {
//vm.animals[animalID - 1].audio += 1
//}
Warning;Result of 'AudioPlayerView' initializer is unused
if animalID == 1 {
AudioPlayerView(audioSelected: vm.animals[animalID -
2].audio, audioTitle: "")
}
}//INDEX
}//ENDVIEW
struct AudioPlayerView: View {
//var AudioPLayer: String
var audioSelected: String
var audioTitle: String
//var audioPlayer: AVAudioPlayer?
#State var audioPlayer: AVAudioPlayer!
var body: some View {
VStack {
HStack {
Spacer()
Button(action: {
self.audioPlayer.play()
}) {
Image(systemName:
"play.circle.fill").resizable()
.frame(width: 50, height: 50)
.aspectRatio(contentMode: .fit)
.accentColor(/
*#START_MENU_TOKEN#*/.black/*#END_MENU_TOKEN#*/)
}
Spacer()
Button(action: {
self.audioPlayer.pause()
}) {
Image(systemName:
"pause.circle.fill").resizable()
.frame(width: 50, height: 50)
.aspectRatio(contentMode: .fit)
.accentColor(/
*#START_MENU_TOKEN#*/.black/*#END_MENU_TOKEN#*/)
}
Spacer()
}//HStack
}//VStack
.onAppear {
let url = Bundle.main.path(forResource: audioSelected,
ofType: "mp3")
self.audioPlayer = try! AVAudioPlayer(contentsOf:
URL(fileURLWithPath: url!))
}
}
//models
import SwiftUI
class DetailViewModel: ObservableObject {
#Published var animals: [DataModel] =
Bundle.main.decode("animals.json")
}
import Foundation
struct Audio: Codable, Identifiable {
let id: String
let name: String
let headline: String
//Computed Property
var thumbnail: String {
"audio-\(id)"
}
}

Related

How do I let text retrieved from Firestore load upon the view being shown?

Intro
Hi there. I recently asked a different question asking how to implement user-triggered FCMs. I quickly realised that in order to implement FCMs I needed to add another feature to my app, which is why I am here now. I'm trying, trust me.
My question / problem
In the picture I attached, you can see the text that is supposed to show when the user clicks on the friends tab. But it doesn't do that. It does when I refresh the list, because I put the function in the refreshable attribute. I did also put the function in the view initialisation, but it doesn't do what I want it to do. So my question would be, a) why it doesn't load? and b) what would be an approach to solve my problem?
Code reference
This function is called in the init{} of the view and .refreshable{} of the list inside the view. (I also tried adding it via .onAppear{} in the NavigationLink of the parent view.)
#State var bestfriend: String = ""
func getBestie() {
let db = Firestore.firestore()
let docRef = db.collection("users").document(email)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let bestie = document.get("bestie") as? String ?? "error: bestie"
bestfriend = String(bestie)
} else {
print("Document does not exist")
}
}
}
Image for reference
Thanks and annotation
Thank you very much in advance, I'm amazed every day by how amazing this community is and how so many people are willing to help. If you need me to add anything else, of course I will do that. I hope I'll be wise enough one day to help other people with their problems as well.
Edit
The view
import Firebase
import FirebaseAuth
import SDWebImage
import SDWebImageSwiftUI
import SwiftUI
struct View_Friend_Tab: View {
#ObservedObject var friends_model = Model_User()
#State var friendWho = ""
init() {
friends_model.getFriendlist()
//friends_model.getBestie()
getBestie()
}
//VARS
let gifurl = URL(string: "https://c.tenor.com/BTCEb08QgBgAAAAC/osita-iheme-aki-and-pawpaw.gif")
let avatarURL = URL(
string:
"https://firebasestorage.googleapis.com/v0/b/universerp-72af2.appspot.com/o/avatars%2Fanime-girl-white-hair-1-cropped.jpg?alt=media&token=efba4215-850d-41c8-8c90-385f7a572e94"
)
#State var showingAlert = false
#State var showFriendRequests = false
#State var bestfriend: String = ""
func getBestie() {
let db = Firestore.firestore()
let docRef = db.collection("users").document(email)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let bestie = document.get("bestie") as? String ?? "error: bestie"
bestfriend = String(bestie)
} else {
print("Document does not exist")
}
}
}
var body: some View {
if !showFriendRequests {
VStack {
NavigationView {
/*
List (friends_model.friend_list) { item in
HStack {
Text(item.email)
Spacer()
}
}
.refreshable{
friends_model.getFriendlist()
}
.listStyle(.grouped)
*/
List {
Section(header: Text("management")) {
NavigationLink(destination: View_Friend_Best_Tab()) {
Label("Select your bestie", systemImage: "star.fill")
}
NavigationLink(destination: View_Friend_Requests_Tab()) {
Label("Manage friend requests", systemImage: "person.fill.questionmark")
}
NavigationLink(destination: View_Friend_Add_Tab()) {
Label("Add friend", systemImage: "person.fill.badge.plus")
}
}
ForEach(friends_model.friend_list) { item in
let avURL = URL(string: item.avatarURL)
Section(header: Text(item.username)) {
HStack {
VStack(alignment: .leading) {
if bestfriend == item.email {
Text("Is your Shin'yū")
.foregroundColor(Color("lightRed"))
.fontWeight(.bold)
.font(.footnote)
}
Text(item.username)
.fontWeight(.bold)
.frame(alignment: .leading)
Text(item.email)
.font(.footnote)
.multilineTextAlignment(.leading)
}
Spacer()
WebImage(url: avURL)
.resizable(resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.clipShape(Circle())
.shadow(radius: 5)
.overlay(Circle().stroke(Color.black, lineWidth: 1))
}
Button("Remove", role: .destructive) {
showingAlert = true
}
.alert("Do you really want to remove this friend?", isPresented: $showingAlert) {
HStack {
Button("Cancel", role: .cancel) {}
Button("Remove", role: .destructive) {
friendWho = item.email
removeFriend()
withAnimation {
friends_model.getFriendlist()
}
}
}
}
}
}
}
.navigationTitle("Your Friends")
.navigationViewStyle(.automatic)
.refreshable {
friends_model.getFriendlist()
getBestie()
}
.listStyle(.insetGrouped)
Spacer()
}
}
} else {
View_Friend_Requests_Tab()
}
}
}
struct View_Friend_Tab_Previews: PreviewProvider {
static var previews: some View {
View_Friend_Tab()
}
}
As previously stated, the function is being called in the init block and when the list is refreshed.
As a general recommendation, use view models to keep your view code clean.
In SwiftUI, a view's initialiser should not perform any expensive / long-running computations. Keep in mind that in SwiftUI, a view is only a description of your UI, not the UI itself. Any state management should be handled outside of the initialiser.
In your case, use .onAppear or .task:
import Firebase
import FirebaseAuth
import SDWebImage
import SDWebImageSwiftUI
import SwiftUI
class FriendsViewModel: ObservableObject {
#Published var bestfriend: String = ""
func getBestie() {
let db = Firestore.firestore()
let docRef = db.collection("users").document(email)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
// consider using Codable for mapping - see https://peterfriese.dev/posts/firestore-codable-the-comprehensive-guide/
let bestie = document.get("bestie") as? String ?? "error: bestie"
self.bestfriend = String(bestie)
} else {
print("Document does not exist")
}
}
}
// add other functions and properties here.
}
struct FriendsView: View {
#ObservedObject var viewModel = FriendsViewModel()
//VARS
let gifurl = URL(string: "https://c.tenor.com/BTCEb08QgBgAAAAC/osita-iheme-aki-and-pawpaw.gif")
let avatarURL = URL(
string:
"https://firebasestorage.googleapis.com/v0/b/universerp-72af2.appspot.com/o/avatars%2Fanime-girl-white-hair-1-cropped.jpg?alt=media&token=efba4215-850d-41c8-8c90-385f7a572e94"
)
#State var showingAlert = false
#State var showFriendRequests = false
#State var bestfriend: String = ""
var body: some View {
if !showFriendRequests {
VStack {
NavigationView {
List {
Section(header: Text("management")) {
NavigationLink(destination: SelectBestieView()) {
Label("Select your bestie", systemImage: "star.fill")
}
NavigationLink(destination: ManageFriendRequestsView()) {
Label("Manage friend requests", systemImage: "person.fill.questionmark")
}
NavigationLink(destination: AddFriendsView()) {
Label("Add friend", systemImage: "person.fill.badge.plus")
}
}
ForEach(viewModel.friends) { friend in
let avURL = URL(string: friend.avatarURL)
Section(header: Text(friend.username)) {
HStack {
VStack(alignment: .leading) {
if bestfriend == friend.email {
Text("Is your Shin'yū")
.foregroundColor(Color("lightRed"))
.fontWeight(.bold)
.font(.footnote)
}
Text(friend.username)
.fontWeight(.bold)
.frame(alignment: .leading)
Text(friend.email)
.font(.footnote)
.multilineTextAlignment(.leading)
}
Spacer()
WebImage(url: avURL)
.resizable(resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.clipShape(Circle())
.shadow(radius: 5)
.overlay(Circle().stroke(Color.black, lineWidth: 1))
}
Button("Remove", role: .destructive) {
showingAlert = true
}
.alert("Do you really want to remove this friend?", isPresented: $showingAlert) {
HStack {
Button("Cancel", role: .cancel) {}
Button("Remove", role: .destructive) {
friendWho = friend.email
viewModel.removeFriend()
withAnimation {
viewModel.getFriendlist()
}
}
}
}
}
}
}
.navigationTitle("Your Friends")
.navigationViewStyle(.automatic)
.onAppear {
viewModel.getFriendlist()
viewModelgetBestie()
}
.refreshable {
viewModel.getFriendlist()
viewModelgetBestie()
}
.listStyle(.insetGrouped)
Spacer()
}
}
} else {
FriendRequestsView()
}
}
}
struct FriendsViewPreviews: PreviewProvider {
static var previews: some View {
FriendsView()
}
}

SwiftUI: How to select multi items(image) with ForEach?

I'm working on my project with the feature of select multiple blocks of thumbnails. Only selected thumbnail(s)/image will be highlighted.
For the ChildView, The binding activeBlock should be turned true/false if a use taps on the image.
However, when I select a thumbnail, all thumbnails will be highlighted.I have come up with some ideas like
#State var selectedBlocks:[Bool]
// which should contain wether or not a certain block is selected.
But I don't know how to implement it.
Here are my codes:
ChildView
#Binding var activeBlock:Bool
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color("orange"))
}
}
}
BlockBView
struct VideoData: Identifiable{
var id = UUID()
var thumbnails: String
}
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "test"), VideoData(thumbnails: "test2"), VideoData(thumbnails: "test1")
]
#State var activeBlock = false
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(0..<videos.count) { _ in
Button(action: {
self.activeBlock.toggle()
}, label: {
ChildView(activeBlock: $activeBlock, thumbnail: "test")
})
}
}
}
}
Thank you for your help!
Here is a demo of possible approach - we initialize array of Bool by videos count and pass activated flag by index into child view.
Tested with Xcode 12.1 / iOS 14.1 (with some replicated code)
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "flag-1"), VideoData(thumbnails: "flag-2"), VideoData(thumbnails: "flag-3")
]
#State private var activeBlocks: [Bool] // << declare
init() {
// initialize state with needed count of bools
self._activeBlocks = State(initialValue: Array(repeating: false, count: videos.count))
}
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(videos.indices, id: \.self) { i in
Button(action: {
self.activeBlocks[i].toggle() // << here !!
}, label: {
ChildView(activeBlock: activeBlocks[i], // << here !!
thumbnail: videos[i].thumbnails)
})
}
}
}
}
}
struct ChildView: View {
var activeBlock:Bool // << value, no binding needed
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color.orange)
}
}
}
}
}
Final result
Build your element and it's model first. I'm using MVVM,
class RowModel : ObservableObject, Identifiable {
#Published var isSelected = false
#Published var thumnailIcon: String
#Published var name: String
var id : String
var cancellables = Set<AnyCancellable>()
init(id: String, name: String, icon: String) {
self.id = id
self.name = name
self.thumnailIcon = icon
}
}
//Equivalent to your BlockView
struct Row : View {
#ObservedObject var model: RowModel
var body: some View {
GroupBox(label:
Label(model.name, systemImage: model.thumnailIcon)
.foregroundColor(model.isSelected ? Color.orange : .gray)
) {
HStack {
Capsule()
.fill(model.isSelected ? Color.orange : .gray)
.onTapGesture {
model.isSelected = !model.isSelected
}
//Two way binding
Toggle("", isOn: $model.isSelected)
}
}.animation(.spring())
}
}
Prepare data and handle action in your parent view
struct ContentView: View {
private let layout = [GridItem(.flexible()),GridItem(.flexible())]
#ObservedObject var model = ContentModel()
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: layout) {
ForEach(model.rowModels) { model in
Row(model: model)
}
}
}
if model.selected.count > 0 {
HStack {
Text(model.selected.joined(separator: ", "))
Spacer()
Button(action: {
model.clearSelection()
}, label: {
Text("Clear")
})
}
}
}
.padding()
.onAppear(perform: prepare)
}
func prepare() {
model.prepare()
}
}
class ContentModel: ObservableObject {
#Published var rowModels = [RowModel]()
//I'm handling by ID for futher use
//But you can convert to your Array of Boolean
#Published var selected = Set<String>()
func prepare() {
for i in 0..<20 {
let row = RowModel(id: "\(i)", name: "Block \(i)", icon: "heart.fill")
row.$isSelected
.removeDuplicates()
.receive(on: RunLoop.main)
.sink(receiveValue: { [weak self] selected in
guard let `self` = self else { return }
print(selected)
if selected {
self.selected.insert(row.name)
}else{
self.selected.remove(row.name)
}
}).store(in: &row.cancellables)
rowModels.append(row)
}
}
func clearSelection() {
for r in rowModels {
r.isSelected = false
}
}
}
Don't forget to import Combine framework.

Why does my array get cleared out when observedobject gets updated?

I'm new to SwiftUI and MVVM and have been working on a podcast app and can't figure out for the life of me how to resolve this issue.
I have a list of podcast episodes with a button assigned to each in a VStack, when pressed, updates the userStore and presents a minimized player. When this happens, my list disappears and I get the ActivityIndicator and the list never reappears. I'm guessing the array gets cleared out whenever the state is updated. I don't want this behavior. What am I doing wrong?
struct PodcastDetailView: View {
#EnvironmentObject var userStore: UserStore
#ObservedObject var minimizableViewHandler: MinimizableViewHandler
#ObservedObject var player: Player = Container.player
#ObservedObject var podcastViewModel: PodcastViewModel
init(podcast: Podcast, player: Player = Container.player, minimizableViewHandler: MinimizableViewHandler) {
self.podcastViewModel = PodcastViewModel(podcast: podcast)
self.player = player
self.minimizableViewHandler = minimizableViewHandler
}
var body: some View {
ZStack{
Color(hex: "1B1D26")
.edgesIgnoringSafeArea([.all])
VStack(alignment: .leading, spacing: 10) {
PodcastDetailHeader(podcast: podcastViewModel.podcast)
if podcastViewModel.episodes.isEmpty {
ActivityIndicator()
.frame(width: 120, height: 120)
.foregroundColor(Color(hex: "813F97"))
.opacity(0.8)
.animation(.easeOut)
} else {
ScrollView {
VStack(alignment: .center, spacing: 10)
{
ForEach(podcastViewModel.episodes, id: \.self) { episode in
Button(action: {
if (self.player.state == .empty) {
self.userStore.selectedEpisode = episode
var newEpisodeData = self.podcastViewModel.episodes
if let selectedEpisodeIndex = newEpisodeData.firstIndex(where: {$0.id == episode.id}) {
newEpisodeData.remove(at: selectedEpisodeIndex)
newEpisodeData.insert(episode, at: newEpisodeData.startIndex)
self.player.setup(for: newEpisodeData)
self.minimizableViewHandler.present()
} else {
// item could not be found
}
} else {
print("new episode is " + episode.title)
self.userStore.selectedEpisode = episode
var newEpisodeData = self.podcastViewModel.episodes
if let selectedEpisodeIndex = newEpisodeData.firstIndex(where: {$0.id == episode.id}) {
newEpisodeData.remove(at: selectedEpisodeIndex)
newEpisodeData.insert(episode, at: newEpisodeData.startIndex)
self.player.setup(for: newEpisodeData)
self.player.play()
}
}
}) {
PodcastRowView(episode: episode)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, 8)
}.buttonStyle(PlainButtonStyle())
.padding(.leading, 20)
.padding(.trailing, 10)
}
}
}
}
Spacer()
}
}
.navigationBarBackButtonHidden(true)
.navigationBarTitle(Text(self.podcastViewModel.podcast.title), displayMode: .inline)
.onAppear {
print("appearing")
self.podcastViewModel.loadEpisodes()
}
}
import Combine
import SwiftUI
class PodcastViewModel: ObservableObject {
private let apiService: APIService
private var episodesCancelable: Cancellable?
#Published var podcast: Podcast
#Published var episodes: [Episode] = []
init(podcast: Podcast, apiService: APIService = APIService()) {
self.podcast = podcast
self.apiService = apiService
}
deinit {
episodesCancelable?.cancel()
}
func loadEpisodes() {
episodesCancelable = apiService.episodes(for: podcast)
.receive(on: RunLoop.main)
.replaceError(with: [])
.assign(to: \.episodes, on: self)
}
}
I took FarouK's advice and used #StateObject. I had to change a couple of things but got it working.
struct PodcastDetailView: View {
#EnvironmentObject var userStore: UserStore
#ObservedObject var minimizableViewHandler: MinimizableViewHandler
#ObservedObject var player: Player = Container.player
#StateObject var podcastViewModel: PodcastViewModel
var podcast: Podcast
var body: some View {
ZStack{
Color(hex: "1B1D26")
.edgesIgnoringSafeArea([.all])
VStack(alignment: .leading, spacing: 10) {
PodcastDetailHeader(podcast: podcastViewModel.podcast)
if podcastViewModel.episodes.isEmpty {
ActivityIndicator()
.frame(width: 120, height: 120)
.foregroundColor(Color(hex: "813F97"))
.opacity(0.8)
.animation(.easeOut)
} else {
ScrollView {
VStack(alignment: .center, spacing: 10)
{
ForEach(podcastViewModel.episodes, id: \.self) { episode in
Button(action: {
if (self.player.state == .empty) {
self.userStore.selectedEpisode = episode
var newEpisodeData = self.podcastViewModel.episodes
if let selectedEpisodeIndex = newEpisodeData.firstIndex(where: {$0.id == episode.id}) {
newEpisodeData.remove(at: selectedEpisodeIndex)
newEpisodeData.insert(episode, at: newEpisodeData.startIndex)
self.player.setup(for: newEpisodeData)
self.minimizableViewHandler.present()
} else {
// item could not be found
}
} else {
print("new episode is " + episode.title)
self.userStore.selectedEpisode = episode
var newEpisodeData = self.podcastViewModel.episodes
if let selectedEpisodeIndex = newEpisodeData.firstIndex(where: {$0.id == episode.id}) {
newEpisodeData.remove(at: selectedEpisodeIndex)
newEpisodeData.insert(episode, at: newEpisodeData.startIndex)
self.player.setup(for: newEpisodeData)
self.player.play()
}
}
}) {
PodcastRowView(episode: episode)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, 8)
}.buttonStyle(PlainButtonStyle())
.padding(.leading, 20)
.padding(.trailing, 10)
}
}
}
}
Spacer()
}
}
.navigationBarBackButtonHidden(true)
.navigationBarTitle(Text(self.podcastViewModel.podcast.title), displayMode: .inline)
.onAppear {
self.podcastViewModel.podcast = self.podcast
self.podcastViewModel.loadEpisodes()
}
}
import Combine
import SwiftUI
class PodcastViewModel: ObservableObject {
private let apiService: APIService
private var episodesCancelable: Cancellable?
#Published var podcast: Podcast = Podcast(id: "", title: "", image: URL(string: ""), thumbnail: URL(string: ""), totalEpisodes: 0, explicitContent: true, description: "", language: "", country: "", rss: URL(string: ""), latestPubDateMs: Date(), earliestPubDateMs: Date(), publisher: "")
#Published var episodes: [Episode] = []
init(apiService: APIService = APIService()) {
self.apiService = apiService
}
deinit {
episodesCancelable?.cancel()
}
func loadEpisodes() {
episodesCancelable = apiService.episodes(for: podcast)
.receive(on: RunLoop.main)
.replaceError(with: [])
.assign(to: \.episodes, on: self)
}
}

SwiftUI => AttributeGraph: cycle detected through attribute after updating one model in list

Context:
I've got a list of custom views. The array is stored a #ObservableObject as #Published.
My custom view has a function which detects when the View is touched (I did it because it's triggered only after an animation). This event activates, through my #ObservableObject, an event that shows a View which is in ZStack with my list. There I could update my passed object through a TextField, and when I come back I have everything updated.
However, when I try to re-show one of every element in my list, my debug shows this error:
AttributeGraph: cycle detected through attribute.
Instead, if I show the detail without updating my model's data, I have not any leak.
Any suggestion?
EDIT:
here's the code:
struct ProcedureList: View {
#ObservedObject var procedureManager = ProcedureManager()
#State private var showModal = false
var isEmpty:Bool {
return procedureManager.procedures.isEmpty
}
init() {
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground()
UINavigationBar.appearance().scrollEdgeAppearance = appearance
UINavigationBar.appearance().standardAppearance = appearance
}
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack {
VStack{
if !self.isEmpty {
List {
ForEach(self.procedureManager.procedures.indices, id: \.self) { index in
ProcedureCell(procedure: self.$procedureManager.procedures[index]){ procedure, position, size in
self.procedureManager.selectedProcedure = procedure
self.procedureManager.cardSize = size
self.procedureManager.cardPosition = position
self.procedureManager.size = size
self.procedureManager.position = position
self.procedureManager.isPressed = true
withAnimation(Animation.default.delay(0.1)) {
self.procedureManager.size.width = geometry.frame(in: .local).width
self.procedureManager.size.height = geometry.frame(in: .local).size.height
self.procedureManager.position.x = geometry.frame(in: .global).origin.x
self.procedureManager.position.y = geometry.frame(in: .global).origin.y
}
print(
"""
pressed procedure: \(procedure.title)
at position: \(position)
and with size: \(size)
"""
)
}
// .tag(self.procedureManager.procedures[index])
.tag(index)
}
.onDelete(perform: self.onDelete)
}
.environment(\.defaultMinListRowHeight, 120)
.animation(.easeInOut)
}else {
VStack{
Text("Non hai ancora creato una procedura!")
.font(.largeTitle)
.multilineTextAlignment(.center)
.padding(.bottom, 30)
Button(action: {
self.showModal.toggle()
}){
Text("Creane una nuova!")
}
.sheet(isPresented: self.$showModal) {
NewProcedure(showModal: self.$showModal) { procedure in
self.procedureManager.newProcedure = procedure
self.procedureManager.createProcedure()
}
}
}.padding(20)
}
}
Rectangle()
.edgesIgnoringSafeArea(.all)
.zIndex(self.procedureManager.isPressed ? 0 : -1)
.opacity(self.procedureManager.isPressed ? 0.7 : 0)
.animation(Animation.easeInOut(duration: 0.5))
ProcedureDetail(action: { procedure in
self.procedureManager.update(procedure: procedure)
}, procedure: self.$procedureManager.selectedProcedure, isShowingDetail: self.$procedureManager.isPressed)
.frame(width: self.procedureManager.correctSize.width, height: self.procedureManager.correctSize.height)
.position(x: self.procedureManager.correctPosition.x, y: self.procedureManager.correctPosition.y - (geometry.frame(in: .global).origin.y))
.offset(x: self.procedureManager.correctSize.width / 2, y: self.procedureManager.correctSize.height / 2)
.animation(.easeInOut)
.opacity(self.procedureManager.correctOpacity)
.animation(Animation.easeInOut.delay(self.procedureManager.isPressed ? 0 : 0.2))
}
.onAppear {
UITableView.appearance().separatorStyle = .none
}
.onDisappear() {
UITableView.appearance().separatorStyle = .singleLine
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarItems(trailing:
!self.isEmpty && !self.procedureManager.isPressed ?
Button(action: {
self.showModal.toggle()
}){
Image(systemName: "plus.circle.fill")
.font(Font.system(size: 40))
.foregroundColor(Color.red)
}
.sheet(isPresented: self.$showModal) {
NewProcedure(showModal: self.$showModal) { procedure in
self.procedureManager.newProcedure = procedure
self.procedureManager.createProcedure()
}
} : nil
)
}
}
}
private func onDelete(offsets: IndexSet) {
self.procedureManager.procedures.remove(atOffsets: offsets)
}
}
struct ProcedureCell: View {
#Binding var procedure: Procedure
#State var position:CGPoint = .zero
#State var size:CGSize = .zero
var action:(_ procedure:Procedure, _ position: CGPoint, _ size:CGSize)->Void
var body: some View {
return
GeometryReader { geometry in
Button(action: {
let position = geometry.frame(in: .global).origin
let size = geometry.frame(in: .global).size
self.action(self.procedure, position, size)
}){
HStack {
VStack(alignment: .leading) {
Text(self.procedure.title)
.font(.largeTitle)
Text(self.procedure.subtitle)
.font(.title)
}
.padding(10)
Spacer()
}
}
.buttonStyle(MyButtonStyle())
.padding([.top, .bottom])
.edgesIgnoringSafeArea(.all)
}
}
}
struct MyButtonStyle:ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.background(
Rectangle()
.fill(configuration.isPressed ? Color.red : Color.orange)
.cornerRadius(20)
.shadow(radius: configuration.isPressed ? 5 : 0)
)
.scaleEffect(configuration.isPressed ? 1.1 : 1)
.animation(.easeInOut)
}
}
struct Procedure: Identifiable {
var title: String
var subtitle: String
var id: String
static var empty:Procedure {
return Procedure(title: "", subtitle: "")
}
init (title:String, subtitle:String) {
self.id = UUID().uuidString
self.title = title
self.subtitle = subtitle
}
}
class ProcedureManager: ObservableObject {
#Published var procedures: [Procedure]
#Published var newProcedure = Procedure.empty
#Published var selectedProcedure = Procedure.empty
#Published var cardSize:CGSize = .zero
#Published var cardPosition:CGPoint = .zero
#Published var size:CGSize = .zero
#Published var position:CGPoint = .zero
#Published var isPressed:Bool = false
var correctSize:CGSize {
if isPressed {
return size
}
else{
return cardSize
}
}
var correctPosition:CGPoint {
if isPressed {
return position
}
else{
return cardPosition
}
}
var correctOpacity: Double {
return isPressed ? 1 : 0
}
func update(procedure:Procedure) {
if let index = procedures.compactMap({$0.id}).firstIndex(of: procedure.id) {
procedures[index].title = procedure.title
procedures[index].subtitle = procedure.subtitle
objectWillChange.send()
}
}
func createProcedure(){
procedures.append(newProcedure)
newProcedure = .empty
}
func createProcedure(with title:String, andSubtitle subtitle:String) {
let procedure = Procedure(title: title, subtitle: subtitle)
procedures.append(procedure)
}
init(){
procedures = [
Procedure(title: "test1", subtitle: "subtitletest1"),
Procedure(title: "test2", subtitle: "subtitletest2"),
Procedure(title: "test3", subtitle: "subtitletest3"),
Procedure(title: "test4", subtitle: "subtitletest4"),
Procedure(title: "test5", subtitle: "subtitletest5"),
]
}
}

View doesn't get updated when using ObservableObject

I'm trying to build an Instagram clone app using SwiftUI.
I'm fetching the data through Firebase and trying to achieve a UI update every time the data in the server changes.
For some reason, when I first open the app and fetch the data, the body of my view gets called, but the UI doesn't change. I even put a breakpoint and saw the body gets called and contains the correct information, it's just the UI which doesn't get updated.
I have a few tabs in my app, and when I switch to another tab (which doesn't contain anything but a Text yet), suddenly the UI does gets updated.
Please see the gif below:
Here is my code:
HomeView:
struct HomeView: View {
#ObservedObject private var fbData = firebaseData
var body: some View {
TabView {
//Home Tab
NavigationView {
ScrollView(showsIndicators: false) {
ForEach(self.fbData.posts.indices, id: \.self) { postIndex in
PostView(post: self.$fbData.posts[postIndex])
.listRowInsets(EdgeInsets())
.padding(.vertical, 5)
}
}
.navigationBarTitle("Instagram", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
print("Camera btn pressed")
}, label: {
Image(systemName: "camera")
.font(.title)
})
, trailing:
Button(action: {
print("Messages btn pressed")
}, label: {
Image(systemName: "paperplane")
.font(.title)
})
)
} . tabItem({
Image(systemName: "house")
.font(.title)
})
Text("Search").tabItem {
Image(systemName: "magnifyingglass")
.font(.title)
}
Text("Upload").tabItem {
Image(systemName: "plus.app")
.font(.title)
}
Text("Activity").tabItem {
Image(systemName: "heart")
.font(.title)
}
Text("Profile").tabItem {
Image(systemName: "person")
.font(.title)
}
}
.accentColor(.black)
.edgesIgnoringSafeArea(.top)
}
}
FirebaseData:
let firebaseData = FirebaseData()
class FirebaseData : ObservableObject {
#Published var posts = [Post]()
let postsCollection = Firestore.firestore().collection("Posts")
init() {
self.fetchPosts()
}
//MARK: Fetch Data
private func fetchPosts() {
self.postsCollection.addSnapshotListener { (documentSnapshot, err) in
if err != nil {
print("Error fetching posts: \(err!.localizedDescription)")
return
} else {
documentSnapshot!.documentChanges.forEach { diff in
if diff.type == .added {
let post = self.createPostFromDocument(document: diff.document)
self.posts.append(post)
} else if diff.type == .modified {
self.posts = self.posts.map { (post) -> Post in
if post.id == diff.document.documentID {
return self.createPostFromDocument(document: diff.document)
} else {
return post
}
}
} else if diff.type == .removed {
for index in self.posts.indices {
if self.posts[index].id == diff.document.documentID {
self.posts.remove(at: index)
}
}
}
}
}
}
}
private func createPostFromDocument(document: QueryDocumentSnapshot) -> Post {
let data = document.data()
let id = document.documentID
let imageUrl = data["imageUrl"] as! String
let authorUsername = data["authorUsername"] as! String
let authorProfilePictureUrl = data["authorProfilePictureUrl"] as! String
let postLocation = data["postLocation"] as! String
let postDescription = data["postDescription"] as! String
let numberOfLikes = data["numberOfLikes"] as! Int
let numberOfComments = data["numberOfComments"] as! Int
let datePosted = (data["datePosted"] as! Timestamp).dateValue()
let isLiked = data["isLiked"] as! Bool
return Post(id: id, imageUrl: imageUrl, authorUsername: authorUsername, authorProfilePictureUrl: authorProfilePictureUrl, postLocation: postLocation, postDescription: postDescription, numberOfLikes: numberOfLikes, numberOfComments: numberOfComments, datePosted: datePosted, isLiked: isLiked)
}
}
If you need me to post more code please let me know.
Update:
PostView:
struct PostView: View {
#Binding var post: Post
var body: some View {
VStack(alignment: .leading) {
//Info bar
HStack {
WebImage(url: URL(string: post.authorProfilePictureUrl))
.resizable()
.frame(width: 40, height: 40)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 2) {
Text(post.authorUsername).font(.headline)
Text(post.postLocation)
}
Spacer()
Button(action: {
print("More options pressed")
}, label: {
Image(systemName: "ellipsis")
.font(.title)
.foregroundColor(.black)
}).buttonStyle(BorderlessButtonStyle())
}
.padding(.horizontal)
//Main Image
WebImage(url: URL(string: post.imageUrl))
.resizable()
.aspectRatio(contentMode: .fit)
//Tools bar
HStack(spacing: 15) {
Button(action: {
self.post.isLiked.toggle()
print("Like btn pressed")
}, label: {
Image(systemName: post.isLiked ? "heart.fill" : "heart")
.font(.title)
.foregroundColor(.black)
}).buttonStyle(BorderlessButtonStyle())
Button(action: {
print("Comments btn pressed")
}, label: {
Image(systemName: "message")
.font(.title)
.foregroundColor(.black)
}).buttonStyle(BorderlessButtonStyle())
Button(action: {
print("Share btn pressed")
}, label: {
Image(systemName: "paperplane")
.font(.title)
.foregroundColor(.black)
}).buttonStyle(BorderlessButtonStyle())
Spacer()
Button(action: {
print("Bookmark btn pressed")
}, label: {
Image(systemName: "bookmark")
.font(.title)
.foregroundColor(.black)
}).buttonStyle(BorderlessButtonStyle())
}.padding(8)
Text("Liked by \(post.numberOfLikes) users")
.font(.headline)
.padding(.horizontal, 8)
Text(post.postDescription)
.font(.body)
.padding(.horizontal, 8)
.padding(.vertical, 5)
Button(action: {
print("Show comments btn pressed")
}, label: {
Text("See all \(post.numberOfComments) comments")
.foregroundColor(.gray)
.padding(.horizontal, 8)
}).buttonStyle(BorderlessButtonStyle())
Text(post.datePostedString)
.font(.caption)
.foregroundColor(.gray)
.padding(.horizontal, 8)
.padding(.vertical, 5)
}
}
}
Post:
struct Post : Identifiable, Hashable {
var id: String
var imageUrl: String
var authorUsername: String
var authorProfilePictureUrl: String
var postLocation: String
var postDescription: String
var numberOfLikes: Int
var numberOfComments: Int
var datePostedString: String
var isLiked: Bool
init(id: String, imageUrl: String, authorUsername: String, authorProfilePictureUrl: String, postLocation: String, postDescription : String, numberOfLikes: Int, numberOfComments: Int, datePosted: Date, isLiked: Bool) {
self.id = id
self.imageUrl = imageUrl
self.authorUsername = authorUsername
self.authorProfilePictureUrl = authorProfilePictureUrl
self.postLocation = postLocation
self.postDescription = postDescription
self.numberOfLikes = numberOfLikes
self.numberOfComments = numberOfComments
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMMM dd, yyyy"
self.datePostedString = dateFormatter.string(from: datePosted)
self.isLiked = isLiked
}
}
Thank you!
The problem is that when the app starts your array is empty, and the ScrollView stops updating, you can replace it for a VStack and it will work (just for testing).
The solution is to wrap the ForEach(or the ScrollView) with a condition, like this:
if (fbData.posts.count > 0) {
ForEach(self.fbData.posts.indices, id: \.self) { postIndex in
PostView(post: self.$fbData.posts[postIndex])
.listRowInsets(EdgeInsets())
.padding(.vertical, 5)
}
}

Resources