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

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.

Related

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

index out of bound from TabViewStyle of SwiftUI

I am trying to implement a tabView that takes a list of items into pages that I could swipe back and forth. However, it keeps bugging out with an "index out of bound" error. It's confusing to me because I never set index at all, and I don't know how to force an index either...
Below are my code. Apologize for any naive code, I am new to SwiftUI. Any helps are appreciated, thank you!
import SwiftUI
//#Published private var list = QuestionList.self
struct QuestionList: Codable {
var list:[QuestionItem]
}
class QuestionItem: Codable, Identifiable {
var id: Int
var text: String
var type: Int
var answer: String
}
struct ContentView: View {
#State private var qlist = [QuestionItem]()
#State private var isShowForm = false
#State private var q1 = true
#State private var answer = ""
#State private var isOn = [Bool]()
#State private var selectedTab = 0
func showForm() {
isShowForm = !isShowForm
let url = URL(string: "http://127.0.0.1:3000/question")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
if let error = error {
print(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print(response)
return
}
guard let data = data else {
return
}
do {
let list = try JSONDecoder().decode([QuestionItem].self, from:data)
qlist = list
print(qlist[1])
for i in 0..<qlist.count {
isOn.append(qlist[i].type == 0)
}
print(isOn)
// print(isOn)
print(type(of: qlist[1]))
} catch {
print("error: ", error)
}
}
task.resume()
}
var body: some View {
Text("Hello, world!")
.padding()
Button("Open Form") {
self.showForm()
}
if (isShowForm) {
TabView(selection: $selectedTab) {
ForEach(qlist.indices, id: \.self) { index in
if qlist[index].type == 0 {
HStack {
Text("\(self.qlist[index].text)")
// Toggle("", isOn: $isOn[index])
Toggle("", isOn: $q1)
}
} else {
VStack {
Text("\(self.qlist[index].text)")
// .lineLimit(2)
// .multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
// .frame(width: 300)
TextField("Enter your answer here:", text: $qlist[index].answer) {
}
}
}
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It is not recommended to use indices in ForEach loops, instead use something like this updated code
that allows you to use your TextField :
ForEach($qlist) { $qItem in // <-- here $
if qItem.type == 0 {
HStack {
Text(qItem.text)
// Toggle("", isOn: $isOn[index])
Toggle("", isOn: $q1)
}
} else {
VStack {
Text(qItem.text)
// .lineLimit(2)
// .multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
// .frame(width: 300)
TextField("Enter your answer here:", text: $qItem.answer) {
}
}
}
}
and as I mentioned in my comment, remove the print(qlist[1]) and print(type(of: qlist[1])) in showForm,
because if qlist is empty or only has one element, you will get the index out of bound error .
Remember one element is qlist[0].
EDIT-1: full test code:
this is the code I used in my test. It does not give any index errors.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var qlist = [QuestionItem]()
#State private var isShowForm = false
#State private var q1 = true
#State private var answer = ""
#State private var isOn = [Bool]()
#State private var selectedTab = 0
func showForm() {
let url = URL(string: "http://127.0.0.1:3000/question")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
if let error = error {
print(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
// print(response)
return
}
guard let data = data else {
return
}
do {
let list = try JSONDecoder().decode([QuestionItem].self, from:data)
qlist = list
// print(qlist[1])
for i in 0..<qlist.count {
isOn.append(qlist[i].type == 0)
}
print(isOn)
// print(isOn)
// print(type(of: qlist[1]))
isShowForm.toggle() // <--- here important
} catch {
print("error: ", error)
}
}
task.resume()
}
var body: some View {
Text("Hello, world!")
.padding()
Button("Open Form") {
// self.showForm()
// simulate showForm()
qlist = [
QuestionItem(id: 0, text: "text0", type: 0, answer: "0"),
QuestionItem(id: 1, text: "text1", type: 1, answer: "1"),
QuestionItem(id: 2, text: "text2", type: 2, answer: "2"),
QuestionItem(id: 3, text: "text3", type: 3, answer: "3")
]
isShowForm.toggle() // <--- here after qlist is set
}
if (isShowForm) {
TabView(selection: $selectedTab) {
ForEach($qlist) { $qItem in
if qItem.type == 0 {
HStack {
Text(qItem.text)
// Toggle("", isOn: $isOn[index])
Toggle("", isOn: $q1)
}
} else {
VStack {
Text(qItem.text)
// .lineLimit(2)
// .multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
// .frame(width: 300)
TextField("Enter your answer here:", text: $qItem.answer)
.border(.red)
}
}
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
}
struct QuestionItem: Codable, Identifiable { // <-- here note the struct
var id: Int
var text: String
var type: Int
var answer: String
}

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 - avoid AnyPublisher the first time

I've created publishers to validate the user input, in this case just validate they have 32 chars length.
ConnectionVM
import UIKit
import Combine
class ConnectionVM: ObservableObject {
private var cancellableSet: Set<AnyCancellable> = []
//INPUT
#Published var uuid1: String = ""
#Published var uuid2: String = ""
//OUTPUT
#Published var uuid1Message = ""
#Published var uuid2Message = ""
init() {
isUUID1ValidPublisher
.receive(on: RunLoop.main)
.map { valid in
valid ? "" : "UUID1 must have 32 characters"
}
.assign(to: \.uuid1Message, on: self)
.store(in: &cancellableSet)
isUUID2ValidPublisher
.receive(on: RunLoop.main)
.map { valid in
valid ? "" : "UUID2 must have 32 characters"
}
.assign(to: \.uuid2Message, on: self)
.store(in: &cancellableSet)
}
private var isUUID1ValidPublisher: AnyPublisher<Bool, Never> {
$uuid1
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.map { input in
return input.count == 32
}
.eraseToAnyPublisher()
}
private var isUUID2ValidPublisher: AnyPublisher<Bool, Never> {
$uuid2
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.map { input in
return input.count == 32
}
.eraseToAnyPublisher()
}
private var isFormValidPublisher: AnyPublisher<Bool, Never> {
Publishers.CombineLatest(isUUID1ValidPublisher, isUUID2ValidPublisher)
.map { uuid1IsValid, uuid2IsValid in
return uuid1IsValid && uuid2IsValid
}
.eraseToAnyPublisher()
}
}
ConnectionView
import SwiftUI
let lightGreyColor = Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0)
struct ConnectionView: View {
#ObservedObject var keyboardResponder = KeyboardResponder()
#ObservedObject var viewModel = ConnectionVM()
// #State var uuid1: String = ""
// #State var uuid2: String = ""
#State var authenticationDidFail: Bool = false
var body: some View {
return VStack {
WelcomeText()
LogoImage()
UUIDTextField(uuid: $viewModel.uuid1)
if !viewModel.uuid1Message.isEmpty {
Text(viewModel.uuid1Message)
.offset(y: -10)
.foregroundColor(.red)
}
UUIDTextField(uuid: $viewModel.uuid2)
if !viewModel.uuid2Message.isEmpty {
Text(viewModel.uuid2Message)
.offset(y: -10)
.foregroundColor(.red)
}
Button(action: {
print("Button tapped")
}) {
LoginButtonContent()
}
}
.padding()
.offset(y: -keyboardResponder.currentHeight*0.5)
}
struct WelcomeText : View {
var body: some View {
return Text("Welcome!")
.font(.largeTitle)
.fontWeight(.semibold)
.padding(.bottom, 20)
}
}
struct LogoImage : View {
var body: some View {
return Image("logo")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 150, height: 150)
.clipped()
.cornerRadius(150)
.padding(.bottom, 75)
}
}
struct UUIDTextField : View {
#Binding var uuid: String
var body: some View {
return TextField("UUID", text: $uuid)
.padding()
.background(lightGreyColor)
.cornerRadius(5.0)
.padding(.bottom, 20)
}
}
struct LoginButtonContent : View {
var body: some View {
return Text("LOGIN")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.green)
.cornerRadius(15.0)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ConnectionView()
}
}
The problem is the first time when the screen is just opened, the error messages appear automatically.
You basically need another condition to guard whether to display the message.
That condition could be that the field has first changed, so you'd count the number of times each field has changed, and produce a Bool on whether it has changed more than once (i.e. beyond the initial assignment of "")
let hasUUID1Changed = $uuid1.scan(0, { a, _ in a + 1}).map { $0 > 1 }
let hasUUID2Changed = $uuid2.scan(0, { a, _ in a + 1}).map { $0 > 1 }
Publishers.Scan acts as the counter / accumulator in this case. The lifetime would be the lifetime of the view model.
Then you could use this publisher in combination with the isUUID1ValidPublisher to determine whether to display the message:
isUUID1ValidPublisher
.receive(on: RunLoop.main)
.combineLatest(hasUUID1Changed) // here
.map { (valid, changed) in
valid && !changed ? "" : "UUID1 must have 32 characters"
}
.sink { [weak self] self?.uuid1Message = $0 }
.store(in: &cancellableSet)

Resources