How do I access only the first element of data in SwiftUI? - ios

So, I have the following View Model where I fetch the data:
import Foundation
import SwiftUI
import Combine
import Alamofire
class AllStatsViewModel: ObservableObject {
#Published var isLoading: Bool = true
#Published var stats = [CountryStats]()
func fetchGlobalStats() {
let request = AF.request("https://projectcovid.deadpool.wtf/all")
request.responseDecodable(of: AllCountryStats.self) { (response) in
guard let globalStats = response.value else { return }
DispatchQueue.main.async {
self.stats = globalStats.data
}
self.isLoading = false
}
}
}
And this is the view:
struct CardView: View {
#ObservedObject var allStatsVM = AllStatsViewModel()
var body: some View {
VStack {
Text(self.allStatsVM.stats[0].country)
}
.onAppear {
self.allStatsVM.fetchGlobalStats()
}
}
}
I'd like to access only the first element of the data, but the problem I face is that when the view loads, the data is not loaded, so I get an index out of range error at
Text(self.allStatsVM.stats[0].country)
Is there a way, I can access the first element?

try this:
struct CardView: View {
#ObservedObject var allStatsVM = AllStatsViewModel()
var body: some View {
VStack {
if self.allStatsVM.stats.count > 0 {
Text(self.allStatsVM.stats[0].country)
} else {
Text ("data loading")
}
}
.onAppear {
self.allStatsVM.fetchGlobalStats()
}
}
}

Related

Why is my .onAppear not getting triggered when an EnvironmentObject changes?

I'm trying to learn SwiftUI, but i can't seem to get my view to update. I want my WorkoutsView to refresh with the newly added workout when the user presses the "Add" button:
WorkoutTrackerApp:
#main
struct WorkoutTrackerApp: App {
var body: some Scene {
WindowGroup {
WorkoutTrackerView()
}
}
}
extension WorkoutTrackerApp {
struct WorkoutTrackerView: View {
#StateObject var workoutService = WorkoutService.instance
var body: some View {
NavigationView {
WorkoutsView { $workout in
NavigationLink(destination: WorkoutView(workout: $workout)){
Text(workout.title)
}
}
.toolbar {
Button("Add") {
workoutService.addNewWorkout()
}
}
.navigationTitle("Workouts")
}
.environmentObject(workoutService)
}
}
}
WorkoutsView:
import Foundation
import SwiftUI
struct WorkoutsView<Wrapper>: View where Wrapper: View {
#EnvironmentObject var workoutService: WorkoutService
#StateObject var viewModel: ViewModel
let workoutWrapper: (Binding<Workout>) -> Wrapper
init(_ viewModel: ViewModel = .init(), workoutWrapper: #escaping (Binding<Workout>) -> Wrapper) {
_viewModel = StateObject(wrappedValue: viewModel)
self.workoutWrapper = workoutWrapper
}
var body: some View {
List {
Section(header: Text("All Workouts")) {
ForEach($viewModel.workouts) { $workout in
workoutWrapper($workout)
}
}
}
.onAppear {
viewModel.workoutService = self.workoutService
viewModel.getWorkouts()
}
}
}
extension WorkoutsView {
class ViewModel: ObservableObject {
#Published var workouts = [Workout]()
var workoutService: WorkoutService?
func getWorkouts() {
workoutService?.getWorkouts { workouts in
self.workouts = workouts
}
}
}
}
WorkoutService:
import Foundation
class WorkoutService: ObservableObject {
static let instance = WorkoutService()
#Published var workouts = [Workout]()
private init() {
for i in 0...5 {
let workout = Workout(id: i, title: "Workout \(i)", exercises: [])
workouts.append(workout)
}
}
func getWorkouts(completion: #escaping ([Workout]) -> Void) {
DispatchQueue.main.async {
completion(self.workouts)
}
}
func addNewWorkout() {
let newWorkout = Workout(title: "New Workout")
workouts = workouts + [newWorkout]
}
}
The .onAppear in WorkoutsView only gets called once - when the view gets initialised for the first time. I want it to also get triggered when workoutService.addNewWorkout() gets called.
FYI: The WorkoutService is a 'mock' service, in the future i want to call an API there.
Figured it out, changed the body of WorkoutsView to this:
var body: some View {
List {
Section(header: Text("All Workouts")) {
ForEach($viewModel.workouts) { $workout in
workoutWrapper($workout)
}
}
}
.onAppear {
viewModel.workoutService = self.workoutService
viewModel.getWorkouts()
}
.onReceive(workoutService.objectWillChange) {
viewModel.getWorkouts()
}
}
Now the workouts list gets refreshed when workoutService publisher emits. The solution involved using the .onReceive to do something when the WorkoutService changes.

How can I dynamically build a View for SwiftUI and present it?

I've included stubbed code samples. I'm not sure how to get this presentation to work. My expectation is that when the sheet presentation closure is evaluated, aDependency should be non-nil. However, what is happening is that aDependency is being treated as nil, and TheNextView never gets put on screen.
How can I model this such that TheNextView is shown? What am I missing here?
struct ADependency {}
struct AModel {
func buildDependencyForNextExperience() -> ADependency? {
return ADependency()
}
}
struct ATestView_PresentationOccursButNextViewNotShown: View {
#State private var aDependency: ADependency?
#State private var isPresenting = false
#State private var wantsPresent = false {
didSet {
aDependency = model.buildDependencyForNextExperience()
isPresenting = true
}
}
private let model = AModel()
var body: some View {
Text("Tap to present")
.onTapGesture {
wantsPresent = true
}
.sheet(isPresented: $isPresenting, content: {
if let dependency = aDependency {
// Never executed
TheNextView(aDependency: dependency)
}
})
}
}
struct TheNextView: View {
let aDependency: ADependency
init(aDependency: ADependency) {
self.aDependency = aDependency
}
var body: some View {
Text("Next Screen")
}
}
This is a common problem in iOS 14. The sheet(isPresented:) gets evaluated on first render and then does not correctly update.
To get around this, you can use sheet(item:). The only catch is your item has to conform to Identifiable.
The following version of your code works:
struct ADependency : Identifiable {
var id = UUID()
}
struct AModel {
func buildDependencyForNextExperience() -> ADependency? {
return ADependency()
}
}
struct ContentView: View {
#State private var aDependency: ADependency?
private let model = AModel()
var body: some View {
Text("Tap to present")
.onTapGesture {
aDependency = model.buildDependencyForNextExperience()
}
.sheet(item: $aDependency, content: { (item) in
TheNextView(aDependency: item)
})
}
}

How to change complex data source in swiftUI List

App crash when I change data source like I tap “change data” button in APIView or delete item in QueryParametersView.list
console log:
This class 'SwiftUI.AccessibilityNode' is not a known serializable
element and returning it as an accessibility element may lead to
crashes
Fatal error: Index out of range: file
/AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.8.25.8/swift/stdlib/public/core/ContiguousArrayBuffer.swift,
line 444
class URLComponentsModel: ObservableObject {
#Published var urlComponents = URLComponents.init()
var urlQueryItems: [URLQueryItem] {
get {
urlComponents.queryItems ?? [URLQueryItem].init()
}
set {
urlComponents.queryItems = newValue
}
}
}
struct APIView: View {
#ObservedObject var urlComponentsModel = URLComponentsModel.init()
var body: some View {
Button.init("change data") {
self.urlComponentsModel.urlComponents.queryItems?.removeFirst()
}
QueryParametersView.init(parameters: self.$urlComponentsModel.urlQueryItems)
}
}
struct QueryParametersView: View {
#Binding var parameters: [URLQueryItem]
var body: some View {
List {
ForEach(self.parameters.indices, id: \.self) { i in
HStack {
ParameterView.init(urlQueryItem: self.$parameters[i])
Text.init("delete")
.onTapGesture {
self.parameters.remove(at: i)
}
}
}
.onDelete { indices in
indices.forEach {
self.parameters.remove(at: $0)
}
}
}
}
struct ParameterView: View {
#Binding var urlQueryItem: URLQueryItem
var body: some View {
ZStack {
...
HStack {
...
if self.urlQueryItem.value != nil {
TextField("Value", text: Binding.init(get: {
(self.urlQueryItem.value ?? "")
}, set: { (value) in
self.urlQueryItem.value = value
}))
}
}
}
}
}
why? anybody help me?
removeFirst()
It says
The collection must not be empty.
If the collection is empty when you call removeFirst, app crashes with index out of range.

How to run a method in ContentView when an ObservedObject changes in other class [Swift 5 iOS 13.4]

Here is my basic ContentView
struct ContentView: View
{
#ObservedObject var model = Model()
init(model: Model)
{
self.model = model
}
// How to observe model.networkInfo's value over here and run "runThis()" whenever the value changes?
func runThis()
{
// Function that I want to run
}
var body: some View
{
VStack
{
// Some widgets here
}
}
}
}
Here is my model
class Model: ObservableObject
{
#Published var networkInfo: String
{
didSet
{
// How to access ContentView and run "runThis" method from there?
}
}
}
I'm not sure if it is accessible ? Or if I can observe ObservableObject changes from View and run any methods?
Thanks in advance!
There are a number of ways to do this. If you want to runThis() when the
networkInfo changes then you could use something like this:
class Model: ObservableObject {
#Published var networkInfo: String = ""
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Button(action: {
self.model.networkInfo = "test"
}) {
Text("change networkInfo")
}
}.onReceive(model.$networkInfo) { _ in self.runThis() }
}
func runThis() {
print("-------> runThis")
}
}
another global way is this:
class Model: ObservableObject {
#Published var networkInfo: String = "" {
didSet {
NotificationCenter.default.post(name: NSNotification.Name("runThis"), object: nil)
}
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Button(action: {
self.model.networkInfo = "test"
}) {
Text("change networkInfo")
}
}.onReceive(
NotificationCenter.default.publisher(for: NSNotification.Name("runThis"))) { _ in
self.runThis()
}
}
func runThis() {
print("-------> runThis")
}
}

Cannot fetch data from Cloud Firestore in SwiftUI Button view

Why does this not work? There is data in Firestore, but it appears the block doesn't execute:
import SwiftUI
import FirebaseFirestore
struct ContentView: View {
var body: some View {
VStack{
Button(
action: {
print("Getting data...")
let db = Firestore.firestore().collection("menu")
let query = db.order(by: "name", descending: true)
query.getDocuments() { snapshot, err in
guard let snapshot = snapshot else {
return
}
print(snapshot)
for doc in snapshot.documents {
print(doc)
}
}
},
label: { Text("Click Me") }
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You shouldn't add data access logic to a button's action handler. Instead, extract data access logic into a view model (or even a repository), and then add a subscription on the view model's properties like this:
Hypothetical menu item data model:
struct MenuItem: Identifiable {
var id: String = UUID().uuidString
var title: String
}
View Model:
import Foundation
import FirebaseFirestore
class MenuItemsViewModel: ObservableObject {
#Published var menuItems = [MenuItem]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("menuitems").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.menuItems = documents.map { queryDocumentSnapshot -> MenuItem in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
return MenuItem(id: .init(), title: title)
}
}
}
}
And in your view:
struct MenuItemsListView: View {
#ObservedObject var viewModel = MenuItemsViewModel()
var body: some View {
NavigationView {
List(viewModel.menuItems) { menuItem in
VStack(alignment: .leading) {
Text(menuItem.title)
.font(.headline)
}
}
.navigationBarTitle("Menu")
.onAppear() { // (3)
self.viewModel.fetchData()
}
}
}
}
Data mapping can be further simplified by using Firestore's Codable support:
Here's how you need to update the model:
import FirebaseFirestoreSwift
struct MenuItem: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
var title: String
}
And here is the updated fetchData() method:
func fetchData() {
db.collection("menuitems").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.menuItems = documents.compactMap { (queryDocumentSnapshot) -> MenuItem? in
return try? queryDocumentSnapshot.data(as: MenuItem.self)
}
}
}
If this doesn't work, check the log for any error messages (your security rules might be preventing you from reading data, for example).

Resources