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

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

Related

Swiftui items get duplicated in all views when added to a single custom view

I'm struggling with the following issue: I'm trying to build a very simple app that lets you add items in a dedicated view that can be collapsed. I managed to write a simple function that lets me add multiple of these custom collapsable views. It's my first app so I wanted to follow the MVVM protocol. I think I got confused along the way because now every item I add gets automatically added to all the custom collapsable views I made. Is there any way to fix this? I thought using the UUID would solve this issue.. I'm guessing that I have to customise the "saveButtonPressed" function, but I don't know how to tell it to only add the item to the view where I pressed the "plus" button..
Here are the Models for the individual items and the collapsable view:
struct ItemModel: Identifiable, Equatable {
let id: String
let title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel {
return ItemModel(id: id, title: title)
}
}
--
import Foundation
struct CollapsableItem: Equatable, Identifiable, Hashable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> CollapsableItem {
return CollapsableItem(id: id, title: title)
}
}
These are my two ViewModels:
class ListViewModel: ObservableObject {
#Published var items: [ItemModel] = []
init() {
getItems()
}
func getItems() {
let newItems = [
ItemModel(title: "List Item1"),
ItemModel(title: "List Item2"),
ItemModel(title: "List Item3"),
]
items.append(contentsOf: newItems)
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
--
class CollapsedViewModel: ObservableObject {
#Published var collapsableItems: [CollapsableItem] = []
#Published var id = UUID().uuidString
init() {
getCollapsableItems()
}
func getCollapsableItems() {
let newCollapsableItems = [
CollapsableItem(title: "Wake up")
]
collapsableItems.append(contentsOf: newCollapsableItems)
}
func addCollapsableItem(title: String) {
let newCollapsableItem = CollapsableItem(title: title)
collapsableItems.append(newCollapsableItem)
}
func updateCollapsableItem(collapsableItem: CollapsableItem) {
if let index = collapsableItems.firstIndex(where: { $0.id ==
collapsableItem.id}) {
collapsableItems[index] =
collapsableItem.updateCompletion()
}
}
}
The item view:
struct ListRowView: View {
#EnvironmentObject var listViewModel: ListViewModel
let item: ItemModel
var body: some View {
HStack() {
Text(item.title)
.font(.body)
.fontWeight(.bold)
.foregroundColor(.white)
.multilineTextAlignment(.center)
.lineLimit(1)
.frame(width: 232, height: 16)
}
.padding( )
.frame(width: 396, height: 56)
.background(.gray)
.cornerRadius(12.0)
}
}
The collapsable view:
struct CollapsedView2<Content: View>: View {
#State var collapsableItem: CollapsableItem
#EnvironmentObject var collapsedViewModel: CollapsedViewModel
#State private var collapsed: Bool = true
#EnvironmentObject var listViewModel: ListViewModel
#State var label: () -> Text
#State var content: () -> Content
#State private var show = true
var body: some View {
ZStack{
VStack {
HStack{
Button(
action: { self.collapsed.toggle() },
label: {
HStack() {
Text("Hello")
.font(.title2.bold())
Spacer()
Image(systemName: self.collapsed ? "chevron.down" :
"chevron.up")
}
.padding(.bottom, 1)
.background(Color.white.opacity(0.01))
}
)
.buttonStyle(PlainButtonStyle())
Button(action: saveButtonPressed, label: {
Image(systemName: "plus")
.font(.title2)
.foregroundColor(.white)
})
}
VStack {
self.content()
}
ForEach(listViewModel.items) { item in ListRowView (item: item)
}
.lineLimit(1)
.fixedSize(horizontal: true, vertical: true)
.frame(minWidth: 396, maxWidth: 396, minHeight: 0, maxHeight: collapsed ?
0 : .none)
.animation(.easeInOut(duration: 1.0), value: show)
.clipped()
.transition(.slide)
}
}
}
func saveButtonPressed() {
listViewModel.addItem(title: "Hello")
}
}
and finally the main view:
struct ListView: View {
#EnvironmentObject var listViewModel: ListViewModel
#EnvironmentObject var collapsedViewModel: CollapsedViewModel
var body: some View {
ZStack{
ScrollView{
VStack{
HStack{
Text("MyFirstApp")
.font(.largeTitle.bold())
Button(action: newCollapsablePressed, label: {
Image(systemName: "plus")
.font(.title2)
.foregroundColor(.white)
})
}
.padding()
.padding(.leading)
ForEach(collapsedViewModel.collapsableItems) { collapsableItem in
CollapsedView2 (collapsableItem: collapsableItem,
label: { Text("") .font(.title2.bold()) },
content: {
HStack {
Text("")
Spacer() }
.frame(maxWidth: .infinity)
})
}
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.statusBar(hidden: false)
.navigationBarHidden(true)
}
}
func newCollapsablePressed() {
collapsedViewModel.addCollapsableItem(title: "hello2")
}
}
Would love to understand how I could fix this!
There is the anwser for your comment about add item in each CollapsedView2.
Because ListViewModel is not ObservableObject (ListViewModel is difference from each CollapsableItem). You should use "#State var items: [ItemModel]".
struct CollapsedView2<Content: View>: View {
#State var collapsableItem: CollapsableItem
// #State var listViewModel = ListViewModel()
#State var collapsed: Bool = true
#State var label: () -> Text
#State var content: () -> Content
#State private var show = true
#State var items: [ItemModel] = []
#State var count = 1
var body: some View {
VStack {
HStack{
Text("Hello")
.font(.title2.bold())
Spacer()
Button( action: { self.collapsed.toggle() },
label: {
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
)
.buttonStyle(PlainButtonStyle())
Button(action: saveButtonPressed, label: {
Image(systemName: "plus")
.font(.title2)
// .foregroundColor(.white)
})
}
VStack {
self.content()
}
ForEach(items) { item in
ListRowView (item: item)
}
.lineLimit(1)
.fixedSize(horizontal: true, vertical: true)
.frame(minHeight: 0, maxHeight: collapsed ? 0 : .none)
.animation(.easeInOut(duration: 1.0), value: show)
.clipped()
.transition(.slide)
}
}
func saveButtonPressed() {
addItem(title: "Hello \(count)")
count += 1
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
There is the anwser. Ask me if you have some questions
struct ListView: View {
#StateObject var collapsedViewModel = CollapsedViewModel()
var body: some View {
ScrollView{
VStack{
HStack{
Text("MyFirstApp")
.font(.largeTitle.bold())
Button(action: newCollapsablePressed, label: {
Image(systemName: "plus")
.font(.title2)
// .foregroundColor(.white)
})
}
ForEach(collapsedViewModel.collapsableItems) { collapsableItem in
CollapsedView2 (collapsableItem: collapsableItem,
label: { Text("") .font(.title2.bold()) },
content: {
HStack {
Text("")
Spacer()
}
})
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.statusBar(hidden: false)
.navigationBarHidden(true)
}
func newCollapsablePressed() {
collapsedViewModel.addCollapsableItem(title: "hello2")
}
}
struct CollapsedView2<Content: View>: View {
#State var collapsableItem: CollapsableItem
#State var listViewModel = ListViewModel()
#State var collapsed: Bool = true
#State var label: () -> Text
#State var content: () -> Content
#State private var show = true
var body: some View {
VStack {
HStack{
Button( action: { self.collapsed.toggle() },
label: {
HStack() {
Text("Hello")
.font(.title2.bold())
Spacer()
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
.padding(.bottom, 1)
.background(Color.white.opacity(0.01))
}
)
.buttonStyle(PlainButtonStyle())
Button(action: saveButtonPressed, label: {
Image(systemName: "plus")
.font(.title2)
.foregroundColor(.white)
})
}
VStack {
self.content()
}
ForEach(listViewModel.items) { item in
ListRowView (item: item)
}
.lineLimit(1)
.fixedSize(horizontal: true, vertical: true)
.frame(minHeight: 0, maxHeight: collapsed ? 0 : .none)
.animation(.easeInOut(duration: 1.0), value: show)
.clipped()
.transition(.slide)
}
}
func saveButtonPressed() {
listViewModel.addItem(title: "Hello")
}
}
struct ListRowView: View {
let item: ItemModel
var body: some View {
HStack() {
Text(item.title)
.font(.body)
.fontWeight(.bold)
.foregroundColor(.white)
.multilineTextAlignment(.center)
.lineLimit(1)
.frame(width: 232, height: 16)
}
.padding( )
.frame(width: 396, height: 56)
.background(.gray)
.cornerRadius(12.0)
}
}
class ListViewModel {
var items: [ItemModel] = []
init() {
getItems()
}
func getItems() {
let newItems = [
ItemModel(title: "List Item1"),
ItemModel(title: "List Item2"),
ItemModel(title: "List Item3"),
]
items.append(contentsOf: newItems)
}
func addItem(title: String) {
let newItem = ItemModel(title: title)
items.append(newItem)
}
func updateItem(item: ItemModel) {
if let index = items.firstIndex(where: { $0.id == item.id}) {
items[index] = item.updateCompletion()
}
}
}
class CollapsedViewModel: ObservableObject {
#Published var collapsableItems: [CollapsableItem] = []
#Published var id = UUID().uuidString
init() {
getCollapsableItems()
}
func getCollapsableItems() {
let newCollapsableItems = [
CollapsableItem(title: "Wake up")
]
collapsableItems.append(contentsOf: newCollapsableItems)
}
func addCollapsableItem(title: String) {
let newCollapsableItem = CollapsableItem(title: title)
collapsableItems.append(newCollapsableItem)
}
func updateCollapsableItem(collapsableItem: CollapsableItem) {
if let index = collapsableItems.firstIndex(where: { $0.id ==
collapsableItem.id}) {
collapsableItems[index] =
collapsableItem.updateCompletion()
}
}
}
struct CollapsableItem: Equatable, Identifiable, Hashable {
let id: String
var title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> CollapsableItem {
return CollapsableItem(id: id, title: title)
}
}
struct ItemModel: Identifiable, Equatable {
let id: String
let title: String
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
func updateCompletion() -> ItemModel {
return ItemModel(id: id, title: title)
}
}

I have a problem with displaying the data inside the sheet

I am trying to display a link from the variable
#StateObject var modelNew = Api()
But I want to take a value in the sheet
Inside the view, I can view the entire content without a problem
struct detiles2: View {
var model : model
#State var isPlayer = false
#StateObject var modelNew = Api()
var body: some View {
VStack {
let link = URL(string: APIgetURL.PathImage + (model.image_path ?? ""))
URLImage(link!) { image in
image
.resizable()
.shadow(color: .black.opacity(0.2), radius: 10, x: 20, y: 20)
.frame(maxWidth: .infinity, maxHeight: 200)
}
Text("Hello").padding()
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(modelNew.models) { item in
HStack {
ZStack {
Circle()
.frame(maxWidth: 30, maxHeight: 30)
.shadow(color: .black.opacity(0.2), radius: 10, x: 20, y: 20)
Image(systemName: "play.fill")
.foregroundColor(Color(UIColor.systemBackground))
.onTapGesture {
isPlayer.toggle()
}
}.padding(.leading, 15)
Spacer()
VStack(alignment: .trailing, spacing: 15) {
Text(item.title)
Text(item.algiment).font(.footnote).foregroundColor(Color.brown)
}.padding(.trailing, 5)
let link = URL(string: APIgetURL.PathImage + (model.image_path ?? ""))
URLImage(link!) { image in
image
.resizable()
.cornerRadius(20)
.frame(maxWidth: 80, maxHeight: 80)
}
}.padding(.trailing, 5)
}
}
}
Spacer()
}
.onAppear() {
modelNew.getData(url: model.url!)
}
.sheet(isPresented: $isPlayer) {
player(url: <String>) //Here ----- I want to pull it out like right away ForEach(modelNew.models) { item in} To get item.url
}
.ignoresSafeArea()
}
}
Here I am fetching new data and it was displayed correctly in the view, but inside the sheet I was not able to do so
class Api : ObservableObject{
#Published var models : [model] = []
func getData (url : String) {
guard let url = URL(string: url) else { return }
var request = URLRequest(url: url)
let token = "38|xxxxx"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, responce, err in
guard let data = data else { return }
print(data)
do {
let dataModel = try JSONDecoder().decode([model].self, from: data)
DispatchQueue.main.async {
self.models = dataModel
}
} catch {
print("error: ", error)
}
}
.resume()
}
}
you could try a different approach using .sheet(item:...) as in
this sample code, to show your sheet with the video player. You should be able to adapt this code to suit your purpose. Works for me.
import Foundation
import AVFoundation
import AVKit
import SwiftUI
struct SiteURL: Identifiable {
let id = UUID()
var urlString: String
}
struct ContentView: View {
#State var sheetUrl: SiteURL?
var body: some View {
Button("show player sheet", action: {
sheetUrl = SiteURL(urlString: "https://jplayer.org/video/m4v/Finding_Nemo_Teaser.m4v")
})
.sheet(item: $sheetUrl) { site in
if let url = URL(string: site.urlString) {
VideoPlayer(player: AVPlayer(url: url))
}
}
}
}
So, in your .onTapGesture {...} set the sheetUrl with your item info, like in the sample code Button,
instead of isPlayer.toggle().
EDIT-1: here is another example code to show that my answer with .sheet(item:...) works.
struct Model: Identifiable, Codable {
let id = UUID()
var image_path: String
var title: String
var algiment: String
var url: String
}
class Api: ObservableObject {
// for testing
#Published var models: [Model] = [
Model(image_path: "image_path-1", title: "Finding_Nemo_Teaser", algiment: "model algiment-1", url: "https://jplayer.org/video/m4v/Finding_Nemo_Teaser.m4v"),
Model(image_path: "image_path-2", title: "Incredibles_Teaser", algiment: "model algiment-2", url: "https://jplayer.org/video/m4v/Incredibles_Teaser.m4v")]
func getData(url: String) {
guard let url = URL(string: url) else { return }
var request = URLRequest(url: url)
let token = "38|xxxxx"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, responce, err in
guard let data = data else { return }
print(data)
do {
let dataModel = try JSONDecoder().decode([Model].self, from: data)
DispatchQueue.main.async {
self.models = dataModel
}
} catch {
print("error: ", error)
}
}
.resume()
}
}
struct TestView: View {
#State var videoUrl: SiteURL? // <-- here
// var model : model
#StateObject var modelNew = Api()
var body: some View {
VStack {
// let link = URL(string: "https://ccc/" + (model.image_path ?? ""))
// URLImage(link!) { image in
// image
// .resizable()
// .shadow(color: .black.opacity(0.2), radius: 10, x: 20, y: 20)
// .frame(maxWidth: .infinity, maxHeight: 200)
// }
Text("Hello").padding()
VStack {
ScrollView(.vertical, showsIndicators: false) {
ForEach(modelNew.models) { item in
HStack {
ZStack {
Circle()
.frame(maxWidth: 30, maxHeight: 30)
.shadow(color: .black.opacity(0.2), radius: 10, x: 20, y: 20)
Image(systemName: "play.fill")
.foregroundColor(Color(UIColor.systemBackground))
.onTapGesture {
videoUrl = SiteURL(urlString: item.url) // <-- here
}
}.padding(.leading, 15)
Spacer()
VStack(alignment: .trailing, spacing: 15) {
Text(item.title)
Text(item.algiment).font(.footnote).foregroundColor(Color.brown)
}.padding(.trailing, 5)
// let link = URL(string: APIgetURL.PathImage + (model.image_path ?? ""))
// URLImage(link!) { image in
// image
// .resizable()
// .cornerRadius(20)
// .frame(maxWidth: 80, maxHeight: 80)
// }
}.padding(.trailing, 5)
}
}
}
Spacer()
}
.onAppear() {
// modelNew.getData(url: model.url!)
}
.sheet(item: $videoUrl) { site in // <-- here
if let url = URL(string: site.urlString) {
VideoPlayer(player: AVPlayer(url: url))
}
}
.ignoresSafeArea()
}
}
struct ContentView: View {
var body: some View {
TestView()
}
}

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.

How to refer to #Published var within: class NetworkManager: ObservableObject

I need to define testData:[Test] that refers to #Published var tests:[Test] within
class NetworkManager: ObservableObject (see code).
I have tried the following definition:
/// The app does not compile with this definition
//let testData:[Test] = NetworkManager(tests: Test)
/// The app works with this definition, but shows no remote json data
let testData:[Test] = NetworkManager().tests
class NetworkManager: ObservableObject {
#Published var tests:[Test] = [Test]()
func getAllTests() {
let file = URLRequest(url: URL(string: "https://my-url/remote.json")!)
let task = URLSession.shared.dataTask(with: file) { (data, _, error) in
guard error == nil else { return }
do {
let tests = try JSONDecoder().decode([Test].self, from: data!)
DispatchQueue.main.async {
self.tests = tests
print(tests)
}
} catch {
print("Failed To decode: ", error)
}
}
task.resume()
}
init() {
getAllTests()
}
init(tests: [Test]) {
self.tests = tests
}
}
The code below works fine
/// The app works with this definition and shows the local json data
let testData:[Test] = load("local.json")
func load<T:Decodable>(_ filename:String, as type:T.Type = T.self) -> T {
let data:Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
However, for the first part I get the error message:
"Cannot convert value of type 'Test.Type' to expected argument type '[Test]'"
What am I missing here? Any help is highly appreciated.
Additional info in response to the answer and question that shows how testData is used:
import SwiftUI
import Combine
struct Test: Hashable, Codable, Identifiable {
var id:Int
var imageName:String
var imageUrl:String
var category:Category
var description:String
enum Category: String, CaseIterable, Codable, Hashable {
case t1 = "test1"
case t2 = "test2"
case t3 = "test3"
}
}
class NetworkManager: ObservableObject {
#Published var tests:[Test] = [Test]()
private var subscriptions = Set<AnyCancellable>()
func getAllTests() {
let file = URLRequest(url: URL(string: "https://my-url/remote.json")!)
URLSession
.shared
.dataTaskPublisher(for: file)
.map(\.data)
.decode(type: [Test].self, decoder: JSONDecoder())
.replaceError(with: [])
.receive(on: RunLoop.main)
.assign(to: \.tests, on: self)
.store(in: &subscriptions)
}
init() {
getAllTests()
}
init(tests: [Test]) {
self.tests = tests
}
}
let testData:[Test] = NetworkManager().tests
struct ContentView: View {
var categories:[String:[Test]] {
.init(
grouping: testData,
by: {$0.category.rawValue}
)
}
var body: some View {
NavigationView{
List (categories.keys.sorted(), id: \String.self) {key in TestRow(categoryName: "\(key) - Case".uppercased(), tests: self.categories[key]!)
.frame(height: 320)
.padding(.top)
.padding(.bottom)
}
.navigationBarTitle(Text("TEST"))
}
}
}
struct TestRow: View {
var categoryName:String
var tests:[Test]
var body: some View {
VStack {
Text(self.categoryName)
.font(.title)
.multilineTextAlignment(.leading)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top) {
ForEach(self.tests, id: \.self) { tests in
NavigationLink(destination:
TestDetail(test: tests)) {
TestItem(test: tests)
.frame(width: 300)
.padding(.trailing, 30)
Spacer()
}}
}
.padding(.leading)
}
}
}
}
struct TestDetail: View {
var test:Test
var body: some View {
List{
ZStack(alignment: .bottom) {
Image(test.imageUrl)
.resizable()
.aspectRatio(contentMode: .fit)
Rectangle()
.padding()
.frame(height: 80.0)
.opacity(0.25)
.blur(radius: 10)
HStack{
VStack(alignment: .leading) {
Text(test.imageName)
.padding()
// .color(.white)
.colorScheme(.light)
.font(.largeTitle)
}
.padding(.leading)
.padding(.bottom)
Spacer()
}
}
.listRowInsets(EdgeInsets())
VStack(alignment: .leading) {
Text(test.description)
// .padding(.bottom)
// .color(.primary)
.colorScheme(.light)
.font(.body)
.lineLimit(nil)
.lineSpacing(12)
HStack {
Spacer()
OrderButton()
Spacer()
}.padding(.top, 50)
}.padding(.top)
.padding(.bottom)
}
.edgesIgnoringSafeArea(.top)
.navigationBarHidden(true)
}
}
struct TestItem: View {
var test:Test
var body:some View{
VStack(spacing: 16.0)
{
Image(test.imageUrl)
.resizable()
.renderingMode(.original)
.aspectRatio(contentMode: .fill)
.frame(width: 300, height: 170)
.cornerRadius(10)
.shadow(radius: 10)
VStack(alignment: .leading, spacing: 5.0)
{
Text(test.imageName)
// .color(.primary)
.font(.headline)
Text(test.description)
.font(.subheadline)
// .color(.secondary)
.multilineTextAlignment(.leading)
.lineLimit(2)
.frame(height: 40)
}
}
}
}
struct OrderButton : View {
var body: some View {
Button(action: {}) {
Text("Order Now")
}.frame(width: 200, height: 50)
.foregroundColor(.white)
.font(.headline)
.background(Color.blue)
.cornerRadius(10)
}
}
class ImageLoader:ObservableObject
{
#Published var data:Data = Data()
func getImage(imageURL:String) {
guard let test = URL(string: imageURL) else { return }
URLSession.shared.dataTask(with: test) { (data, response, error) in
DispatchQueue.main.async {
if let data = data {
self.data = data
}
}
print(data as Any)
}.resume()
}
init(imageURL:String) {
getImage(imageURL: imageURL)
}
}
struct ContentView_Previews: PreviewProvider {
#ObservedObject var imageLoader: ImageLoader
init(test:String)
{
imageLoader = ImageLoader(imageURL: test)
}
static var previews: some View {
ContentView()
}
}
// local.json
[
{
"id":101,
"imageName":"test-f1a",
"imageUrl":"test-f1a",
"description":"test1a",
"category":"test1"
},
...
]
// remote.json
[
{
"id":101,
"imageName":"test-f1a",
"imageUrl":"https://my-url/test-f1a",
"description":"test1a",
"category":"test1"
},
...
]
As of iOS 13 URLSession has been extended with a publisher, so idiomatically your code becomes:
import UIKit
import Combine
struct Test: Codable {
var name: String
}
class NetworkManager: ObservableObject {
#Published var tests:[Test] = [Test]()
private var subscriptions = Set<AnyCancellable>()
func getAllTests() {
let file = URLRequest(url: URL(string: "https://my-url/remote.json")!)
URLSession
.shared
.dataTaskPublisher(for: file)
.map(\.data)
.decode(type: [Test].self, decoder: JSONDecoder())
.replaceError(with: [])
.receive(on: RunLoop.main)
.assign(to: \.tests, on: self)
.store(in: &subscriptions)
}
init() {
getAllTests()
}
init(tests: [Test]) {
self.tests = tests
}
}
Replacing "grouping: testData" with "grouping: networkManager.tests" and by using "#ObservedObject var networkManager: NetworkManager = NetworkManager()" makes the definition of testData redundant and thus solves the problem. Thanks to #Josh Homann for his answer and comment which helped me to overcome this issue.

Resources