I have three views: ExerciseList View, ExerciseView, and SetsView. The transition between two of them takes a long time (5-15 seconds), and I'm trying to diagnose and address why.
ExerciseListView lists the exercises and lets the user start a session (whose returned session_id is used by the the later Views), and ExerciseView contains a SetsView for each of the exercises. In addition I have an exerciseViewModel, that GETs the exercises from an API and POSTs a session to the API. Here is the code:
struct ExerciseListView: View {
#StateObject var exerciseViewModel = ExerciseViewModel()
var workout: Workout
var body: some View {
NavigationStack {
ScrollView {
VStack{
ForEach(exerciseViewModel.exercises, id: \.exercise_id) { exercise in
exerciseListRow(exercise: exercise)
}
}.navigationTitle(workout.workout_name)
.toolbar{startButton}
}
}.onAppear {
self.exerciseViewModel.fetch(workout_id: workout.workout_id)
}
}
var startButton: some View {
NavigationLink(destination: ExerciseView(exerciseViewModel: exerciseViewModel, workout: workout)) {
Text("Start Workout")
}
}
}
struct exerciseListRow: View {
let exercise: Exercise
var body: some View {
Text(String(exercise.set_number) + " sets of " + exercise.exercise_name + "s").padding(.all)
.font(.title2)
.fontWeight(.semibold)
.frame(width: 375)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(10.0)
}
}
struct Exercise: Hashable, Codable {
var exercise_id: Int
var exercise_name: String
var set_number: Int
}
class ExerciseViewModel: ObservableObject {
var apiManager = ApiManager()
#Published var exercises: [Exercise] = []
#Published var session_id: Int = -1
func fetch(workout_id: Int) {
self.apiManager.getToken()
print("Calling workout data with workout_id " + String(workout_id))
guard let url = URL(string: (self.apiManager.myUrl + "/ExercisesInWorkout"))
else {
print("Error: Something wrong with url.")
return
}
var urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
urlRequest.allHTTPHeaderFields = [
"Token": self.apiManager.token
]
urlRequest.httpMethod = "POST"
let body = "workout_id="+String(workout_id)
urlRequest.httpBody = body.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: urlRequest) { [weak self] data, _, error in
guard let data = data, error == nil else {
return
}
//Convert to json
do {
let exercises = try JSONDecoder().decode([Exercise].self, from: data)
DispatchQueue.main.async {
// print(exercises)
self?.exercises = exercises
}
}
catch {
print("Error: something went wrong calling api", error)
}
}
task.resume()
}
func sendSession(workout_id: Int) {
self.apiManager.getToken()
print("Sending session with workout_id " + String(workout_id))
guard let url = URL(string: (self.apiManager.myUrl + "/Session"))
else {
print("Error: Something wrong with url.")
return
}
var urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
urlRequest.allHTTPHeaderFields = [
"Token": self.apiManager.token
]
urlRequest.httpMethod = "POST"
let body = "workout_id="+String(workout_id)
urlRequest.httpBody = body.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: urlRequest) { [weak self] data, _, error in
guard let data = data, error == nil else {
return
}
do {
let decoded = try JSONDecoder().decode(Int.self, from: data)
DispatchQueue.main.async {
self?.session_id = decoded
}
}
catch {
print("Error: something went wrong calling api", error)
}
}
task.resume()
}
}
struct ExerciseView: View {
#StateObject var exerciseViewModel = ExerciseViewModel()
var workout: Workout
#State var session_started: Bool = false
var body: some View {
NavigationStack {
VStack {
List {
Section(header: Text("Enter exercise data")) {
ForEach(exerciseViewModel.exercises, id: \.exercise_id) { exercise in
NavigationLink(destination: SetsView(workout: workout, exercise: exercise, session_id: exerciseViewModel.session_id)) {
Text(exercise.exercise_name)
}
}
}
}.listStyle(GroupedListStyle())
}.navigationTitle(workout.workout_name)
}.onAppear {
if !self.session_started {
self.exerciseViewModel.sendSession(workout_id: workout.workout_id)
self.session_started = true
}
}
}
}
struct SetsView: View {
var workout: Workout
var exercise: Exercise
var session_id: Int
#ObservedObject var setsViewModel: SetsViewModel
#State var buttonText: String = "Submit All"
#State var lastSetsShowing: Bool = false
init(workout: Workout, exercise: Exercise, session_id: Int) {
print("Starting init for sets view with exercise with session:", session_id, exercise.exercise_name)
self.workout = workout
self.exercise = exercise
self.session_id = session_id
self.setsViewModel = SetsViewModel(session_id: session_id, workout_id: workout.workout_id, exercise: exercise)
}
var body: some View {
ScrollView {
VStack {
ForEach(0..<exercise.set_number) {set_index in
SetView(set_num: set_index, setsViewModel: setsViewModel)
Spacer()
}
submitButton
}.navigationTitle(exercise.exercise_name)
.toolbar{lastSetsButton}
}.sheet(isPresented: $lastSetsShowing) { LastSetsView(exercise: self.exercise, workout_id: self.workout.workout_id)
}
}
var lastSetsButton: some View {
Button("Show Last Sets") {
self.lastSetsShowing = true
}
}
var submitButton: some View {
Button(self.buttonText) {
if entrysNotNull() {
self.setsViewModel.postSets()
self.buttonText = "Submitted"
}
}.padding(.all).foregroundColor(Color.white).background(Color.blue).cornerRadius(10)
}
func entrysNotNull() -> Bool {
if (self.buttonText != "Submit All") {return false}
for s in self.setsViewModel.session.sets {
if ((s.weight == nil || s.reps == nil) || (s.quality == nil || s.rep_speed == nil)) || (s.rest_before == nil) {
return false}
}
return true
}
}
My issue is that there is a large lag after hitting the "Start Workout" button in ExerciseListView before it opens ExerciseView. It has to make an API call and load a view for each exercise in the workout, but considering the most this is is like 7, it is odd it takes so long.
When the Start Button is clicked here is an example response:
Starting init for sets view with exercise with session: -1 Bench Press
Starting init for sets view with exercise with session: -1 Bench Press
Starting init for sets view with exercise with session: -1 Pull Up
Starting init for sets view with exercise with session: -1 Pull Up
Starting init for sets view with exercise with session: -1 Incline Dumbell Press
Starting init for sets view with exercise with session: -1 Incline Dumbell Press
Starting init for sets view with exercise with session: -1 Dumbell Row
Starting init for sets view with exercise with session: -1 Dumbell Row
2023-01-20 00:54:09.174902-0600 BodyTracker[4930:2724343] [connection] nw_connection_add_timestamp_locked_on_nw_queue [C1]
Hit maximum timestamp count, will start dropping events
Starting init for sets view with exercise with session: -1 Shrugs
Starting init for sets view with exercise with session: -1 Shrugs
Starting init for sets view with exercise with session: -1 Decline Dumbell Press
Starting init for sets view with exercise with session: -1 Decline Dumbell Press
Sending session with workout_id 3
Starting init for sets view with exercise with session: 102 Bench Press
Starting init for sets view with exercise with session: 102 Pull Up
Starting init for sets view with exercise with session: 102 Incline Dumbell Press
Starting init for sets view with exercise with session: 102 Dumbell Row
Starting init for sets view with exercise with session: 102 Shrugs
Starting init for sets view with exercise with session: 102 Decline Dumbell Press
Starting init for sets view with exercise with session: 102 Bench Press
Starting init for sets view with exercise with session: 102 Pull Up
Starting init for sets view with exercise with session: 102 Incline Dumbell Press
Starting init for sets view with exercise with session: 102 Dumbell Row
Starting init for sets view with exercise with session: 102 Shrugs
Starting init for sets view with exercise with session: 102 Decline Dumbell Press
Why does the init statement get repeated so many times? Instead of 6 initializations for the 6 exercises, it's 24. And I'm assuming the first 12 inits are with -1 because that's what I instantiate session_id to in exerciseViewModel, but is there a better way to make it work? I've tried using DispatchSemaphore, but the App just gets stuck for some reason. Should I pass around the whole viewmodel instead of just the id? Or perhaps the ag is created by something else I'm missing. I'm fairly confident it's not the API, as non of the other calls take any significant time. Please help me with the right way to set this up.
ExerciseViewModel should be a state object at the top of your view hierarchy, and an observable object thereafter, so
#StateObject var exerciseViewModel = ExerciseViewModel()
In ExerciseView should be
#ObservedObject var exerciseViewModel: ExerciseViewModel
You want the same instance to be used everywhere.
ExerciseListView.workout seems a bit of a strange property, it's not clear where that comes from but if it's something immutable that's set from outside, make it a let.
Don't concern yourself much with the number of times a SwiftUI view is initialised. The framework will be rebuilding the view hierarchy whenever it observes a change in a published or state or other special property. Your views should essentially be free to create, in that you shouldn't be doing anything heavy in the init methods.
Which makes me suspect that this line:
self.setsViewModel = SetsViewModel(session_id: session_id, workout_id: workout.workout_id, exercise: exercise)
Could be part of your problem. You don't show the implementation of this, but if you're creating it on initialisation then it should be a state object, which you create like this:
#StateObject var setsViewModel: SetsViewModel
...
//in your init
_setsViewModel = StateObject(wrappedValue: SetsViewModel(session_id: session_id, workout_id: workout.workout_id, exercise: exercise))
This ensures the view model will only be created once.
The last thing that looks a bit suspect is apiManager.getToken() - this looks like it may well involve an API call, but you are not treating it as asynchronous.
To really work out what's happening, you're off to a good start with the logging, but you could add more. You can also put some breakpoints and step through the code, perhaps pause the program in the debugger while it's hanging, and check the CPU usage or profile in instruments.
Related
I can't figure out how to display all data when searching/filtering the character data. I'm using the Rick and Morty API and want to filter the characters in the search bar. I currently can filter characters but it does not display all since the JSON has multiple pages. I'm not sure how to get more information from all the other pages when filtering.
ViewModel
import Foundation
#MainActor
class CharacterViewModel: ObservableObject {
#Published var characters: [Character] = []
//Don't need an init() method as all properties of this class has default values
//Using concurrency features
#Published var searchText = ""
var filteredCharacters: [Character] {
if searchText.isEmpty {
return characters
} else {
return characters.filter {
$0.name.localizedCaseInsensitiveContains(searchText)
}
}
}
private(set) var maxPages = 1
private(set) var currentPage = 1
private(set) var hasPreviousPage: Bool = false
private(set) var hasNextPage: Bool = true
struct CharacterResults: Codable {
var info: Info
var results: [Character]
struct Info: Codable {
var pages: Int
var next: URL?
var prev: URL?
}
}
//MARK: - Fetch all Characters
func fetchallCharacters() async {
guard let url = URL(string: "https://rickandmortyapi.com/api/character/?page=\(currentPage)&name=\(searchText.trimmed())") else {
fatalError("Bad URL")
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decodedCharacters = try JSONDecoder().decode(CharacterResults.self, from: data)
maxPages = decodedCharacters.info.pages
hasPreviousPage = decodedCharacters.info.prev != nil
hasNextPage = decodedCharacters.info.next != nil
characters = decodedCharacters.results
} catch {
characters = []
}
}
//MARK: - Next page
func nextPage() async {
//Update current page and fetch JSON
currentPage += 1
await fetchallCharacters()
}
//MARK: - Previous page
func previousPage() async {
//Update current page and fetch JSON
currentPage -= 1
await fetchallCharacters()
}
}
ContentView
struct ContentView: View {
#StateObject var model = CharacterViewModel()
#StateObject var favorites = Favorites()
var previousButton: some View {
Button("Previous") {
Task {
await model.previousPage()
}
}
.disabled(!model.hasPreviousPage)
}
var nextButton: some View {
Button("Next") {
Task {
await model.nextPage()
}
}
.disabled(!model.hasNextPage)
}
//MARK: - Body
var body: some View {
NavigationView {
List(model.filteredCharacters){
character in
NavigationLink {
CharacterDetailsView(character: character)
} label: {
HStack{
CharacterRowView(imageUrlString: character.image, name: character.name, species: character.species)
if favorites.contains(character) {
Spacer()
Image(systemName: "heart.fill")
.accessibilityLabel("This is a favorite character")
.foregroundColor(.red)
}
}
}
}
.searchable(text: $model.searchText, prompt: "Search for a character")
.onChange(of: model.searchText, perform: { newValue in
Task {
await model.fetchallCharacters()
}
})
.toolbar{
ToolbarItem(placement: .navigationBarLeading) {
previousButton
}
ToolbarItem(placement: .navigationBarTrailing) {
nextButton
}
}
.navigationBarTitle("Characters")
}//Navigationview
.task({
await model.fetchallCharacters()
})
.phoneOnlyNavigationView()
.environmentObject(favorites)
}
}
This is a common thing that you'll need to be familiar with working with API endpoints. Usually there are a couple of ways to get data from a paginated API. Most commonly there is a Pages value. Looking at the docs there is indeed one of those values on the Characters, as seen here. https://rickandmortyapi.com/documentation/#get-all-characters and the property says there are 42 pages.
"info": {
"count": 826,
"pages": 42,
"next": "https://rickandmortyapi.com/api/character/?page=2",
"prev": null
},
So that means when you call your API endpoint, you need to iterate over it with a function, 42x and add those values to some [Character] array or whatever object you're looking for.
To further illustrate this solution, here's your API URL.
https://rickandmortyapi.com/api/character/?page=\(currentPage)&name=\(searchText.trimmed())
You're already referencing currentPage as a value there. Also if you want all characters, you'd remove the name and simply grab ALL of them. This is quite an expensive operation doing it this way so your best bet is to see if you can add additional properties to that URL request to reduce the pages from 42 to something more manageable. Looking further we have this documentation here, https://rickandmortyapi.com/documentation/#filter-characters which tells you how to filter your results. Which may help you reduce the total cost of the results.
In short, fetch all characters immediately, given your possible filters the endpoint accepts, store them into an array, and then filter on that array. The way that you're doing it currently is to fetch each time you search, which will only return that one result, which may be what you want, but the Docs will provide more context on that.
Possible Solution
You need to modify this function and it's API URL to something like this. Notice I've changed the arguments for fetchAllCharacters to include a page that you can pass in. Also I am telling it to check if there is a .next page, then recursively call fetchAllCharacters(page + 1) which will make the function run again, grabbing more characters. Next instead of setting characters = results I'm appending, that way when the function runs again, it doesn't overwrite previous results.
func fetchallCharacters(page: Int = 1) async {
guard let url = URL(string: "https://rickandmortyapi.com/api/character/?page=\(page))") else {
fatalError("Bad URL")
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decodedCharacters = try JSONDecoder().decode(CharacterResults.self, from: data)
maxPages = decodedCharacters.info.pages
// Append all the characters from this page.
// MAKE SURE `characters` has been initialized to []
for result in decodedCharacters.result {
characters.append(result)
}
hasPreviousPage = decodedCharacters.info.prev != nil
if decodedCharacters.info.next != nil {
fetchAllCharacters(page: page+1)
}
} catch {
characters = []
}
}
I am trying to create a view that displays results from an API call, however I keep on running into multiple errors.
My question is basically where is the best place to make such an API call.
Right now I am "trying" to load the data in the "init" method of the view like below.
struct LandingView: View {
#StateObject var viewRouter: ViewRouter
#State var user1: User
#State var products: [Product] = []
init(_ viewRouter : ViewRouter, user: User) {
self.user1 = user
_viewRouter = StateObject(wrappedValue: viewRouter)
ProductAPI().getAllProducts { productArr in
self.products = productArr
}
}
var body: some View {
tabViewUnique(prodArrParam: products)
}
}
I keep on getting an "escaping closure mutating self" error, and while I could reconfigure the code to stop the error,I am sure that there is a better way of doing what I want.
Thanks
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.title)
}
// this one onAppear you can use it
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data) {
DispatchQueue.main.async {
self.results = response
}
return
}
}
}.resume()
}
}
In .onAppear you can make api calls
I have a list of URLMedia items, which can be a video or Image from and URL in the internet. I want to show to the user, and for improving the user experience, I want to catch the media locally, so the user can see the next media item immediately without needed to download it.
The interface is ready for testing the functionality. You have the media viewer, and down 2 buttons: 1 for go to next media, one for catch the data. There are 4 items as example, 2 videos 2 images, and you can go in loop over the 4 items over and over.
So far the functionality for the photos works fine, you can perceive the speed difference between going to next media before catching the data and after catching it, is immediate. The problem comes with the video, it does not work, and I can't figure out why. There are some limitations about the tempo folder I am unaware of?
This is the main view
struct ContentView: View {
#StateObject private var AS: AppState = AppState.singleton
func downloadUsingAlamofire(urlInput: URL, index: Int) {
AF.download(urlInput).responseURL { response in
// Read file from provided file URL.
AS.models[index].URLCachedMedia = response.fileURL!.absoluteString
print("Done downloading: \(AS.getCurrentModel().URLCachedMedia)")
print("In task: \(index)")
}
}
var body: some View {
VStack {
switch AS.currentType {
case .image:
AsyncImage(url: URL(string: AS.getCurrentModel().URLCachedMedia))
case .video:
PlayerView(videoURL: AS.getCurrentModel().URLCachedMedia)
}
HStack (spacing: 30) {
Button("Next model") {
AS.nextModel()
print("The URL of the model is: \(AS.getCurrentModel().URLCachedMedia)")
}
Button("Cach media") {
for (index, model) in AS.models.enumerated() {
downloadUsingAlamofire(urlInput: URL(string: model.URLMedia)!, index: index)
}
}
}
}
}
}
Video player view, optimise to auto-play when the video is ready:
struct PlayerView: View {
var videoURL : String
#State private var player : AVPlayer?
var body: some View {
VideoPlayer(player: player)
.onAppear() {
// Start the player going, otherwise controls don't appear
guard let url = URL(string: videoURL) else {
return
}
let player = AVPlayer(url: url)
self.player = player
player.play()
}
.onDisappear() {
// Stop the player when the view disappears
player?.pause()
}
}
}
This is the State of the app
class AppState: ObservableObject {
static let singleton = AppState()
private init() {}
#Published var models: [Model] = getModels()
#Published var currentModel: Int = 0
#Published var currentType: TypeMedia = getModels()[0].type
func nextModel() {
if currentModel < models.count - 1 {
currentModel += 1
} else {
currentModel = 0
}
currentType = getCurrentModel().type
}
func getCurrentModel() -> Model {
return models[currentModel]
}
}
And this is the model, with the demo data to test
enum TypeMedia {
case image, video
}
class Model: ObservableObject {
let URLMedia: String
#Published var URLCachedMedia: String
let type: TypeMedia
init(URLMedia: String, type: TypeMedia) {
self.URLMedia = URLMedia
self.type = type
self.URLCachedMedia = self.URLMedia
}
}
func getModels() -> [Model] {
let model1 = Model(URLMedia: "https://storage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", type: .video)
let model2 = Model(URLMedia: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4", type: .video)
let model3 = Model(URLMedia: "https://i0.wp.com/www.wikiwiki.in/wp-content/uploads/2021/09/Palki-Sharma-Upadhyay-Journalist-4.jpg?resize=761.25%2C428&ssl=1", type: .image)
let model4 = Model(URLMedia: "https://static.dw.com/image/60451375_403.jpg", type: .image)
return [model1, model3, model2, model4]
}
I'm not sure what the problem is with my project. Basically, I have a pretty typical data structure: I'm using Firestore for a real-time doc based database, and I have a bunch of different collections and docs and fields. Just very simple, nothing crazy.
I have some model classes, and then some ViewModels to fetch data, filter, add documents to Firestore, and so on.
The app itself is almost 100% SwiftUI, and I'd like to keep it that way, just a challenge for my own development. I've hit a bit of a wall though.
In the app, I have a series of Views with NavigationView Lists that I pass small pieces of data to as you navigate. An example (this is for educational institutions) might be List of Schools > List of Classes > List of Students in Class > List of Grades for Student. Basically the perfect use for a navigation view and a bunch of lists.
The bug:
When I move from one list to the next in the stack, I fetch the firestore data, which loads for a second (enough that the list populates), and then "unloads" back to nothing. Here is some code where this happens (I've cleaned it up to make it as simple as possible):
struct ClassListView: View {
let schoolCode2: String
let schoolName2: String
#ObservedObject private var viewModelThree = ClassViewModel()
var body: some View {
VStack{
List{
if viewModelThree.classes.count > 0{
ForEach(self.viewModelThree.classes) { ownedClass in
NavigationLink(destination: StudentListView()){
Text(ownedClass.className)
}
}
} else {
Text("No Classes Found for \(schoolName2)")
}
}
}
.navigationBarTitle(schoolName2)
.onAppear(){
print("appeared")
self.viewModelThree.fetchData(self.schoolCode2)
}
}
}
So that's the ClassListView that I keep having issues with. For debugging, I added the else Text("No Classes Found") line and it does in fact show. So basically, view loads (this is all in a Nav view from a parent), it fetches the data, which is shown for a second (list populates) and then unloads that data for some reason, leaving me with just the "No classes found".
For more context, here is the code for the ClassViewModel (maybe that's where I'm going wrong?):
struct Classes: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
var schoolCode: String
var className: String
}
enum ClassesCodingKeys: String, CodingKey {
case id
case schoolCode
case className
}
class ClassViewModel: ObservableObject {
#Published var classes = [Classes]()
private var db = Firestore.firestore()
func fetchData(_ schoolCode: String) {
db.collection("testClasses")
.order(by: "className")
.whereField("schoolCode", isEqualTo: schoolCode)
.addSnapshotListener{ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("no docs found")
return
}
self.classes = documents.compactMap{ queryDocumentSnapshot -> Classes? in
return try? queryDocumentSnapshot.data(as: Classes.self)
}
}
}
func addClass(currentClass: Classes){
do {
let _ = try db.collection("testClasses").addDocument(from: currentClass)
}
catch {
print(error)
}
}
}
Most relevant bit is the fetchData() function above.
Maybe the problem is in the view BEFORE this (the parent view?). Here it is:
struct SchoolUserListView: View {
#State private var userId: String?
#EnvironmentObject var session: SessionStore
#ObservedObject private var viewModel = UserTeacherViewModel()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State private var counter = 0
#State private var showingAddSchool: Bool = false
#Environment(\.presentationMode) var presentationMode
func getUser() {
session.listen()
}
var body: some View {
VStack{
List{
if viewModel.teachers.count > 0{
ForEach(viewModel.teachers) { ownedClass in
NavigationLink(destination: ClassListView(schoolName2: ownedClass.schoolName, schoolCode2: ownedClass.schoolCode)){
Text(ownedClass.schoolName)
}
}
} else {
Button(action:{
self.presentationMode.wrappedValue.dismiss()
}){
Text("You have no schools, why not add one?")
}
}
}
.navigationBarTitle("Your Schools")
}
.onAppear(){
self.getUser()
self.userId = self.session.session?.uid
}
.onReceive(timer){ time in
if self.counter == 1 {
self.timer.upstream.connect().cancel()
} else {
print("fetching")
self.viewModel.fetchData(self.userId!)
}
self.counter += 1
}
}
}
And the FURTHER Parent to that View (and in fact the starting view of the app):
struct StartingView: View {
#EnvironmentObject var session: SessionStore
func getUser() {
session.listen()
}
var body: some View {
NavigationView{
VStack{
Text("Welcome!")
Text("Select an option below")
Group{
NavigationLink(destination:
SchoolUserListView()
){
Text("Your Schools")
}
NavigationLink(destination:
SchoolCodeAddView()
){
Text("Add a School")
}
Spacer()
Button(action:{
self.session.signOut()
}){
Text("Sign Out")
}
}
}
}
.onAppear(){
self.getUser()
}
}
}
I know that is a lot, but just so everyone has the order of parents:
StartingView > SchoolUserListView > ClassListView(BUG occurs here)
By the way, SchoolUserListView uses a fetchData method just like ClassListView(where the bug is) and outputs it to a foreach list, same thing, but NO problems? I'm just not sure what the issue is, and any help is greatly appreciated!
Here is a video of the issue:
https://imgur.com/a/kYtow6G
Fixed it!
The issue was the EnvironmentObject for presentationmode being passed in the navigation view. That seems to cause a LOT of undesired behavior.
Be careful passing presentationMode as it seems to cause data to reload (because it is being updated).
I want to present a view after I receive the data from a request, something like this
var body: some View {
VStack {
Text("Company ID")
TextField($companyID).textFieldStyle(.roundedBorder)
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
DispatchQueue.main.async {
self.presentation(Modal(LogonView(), onDismiss: {
print("dismiss")
}))
}
}.resume()
}
}
Business logic mixed with UI code is a recipe for trouble.
You can create a model object as a #ObjectBinding as follows.
class Model: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var shouldPresentModal = false {
didSet {
didChange.send(())
}
}
func fetch() {
// Request goes here
// Edit `shouldPresentModel` accordingly
}
}
And the view could be something like...
struct ContentView : View {
#ObjectBinding var model: Model
#State var companyID: String = ""
var body: some View {
VStack {
Text("Company ID")
TextField($companyID).textFieldStyle(.roundedBorder)
if (model.shouldPresentModal) {
// presentation logic goes here
}
}.onAppear {
self.model.fetch()
}
}
}
The way it works:
When the VStack appears, the model fetch function is called and executed
When the request succeeds shouldPresentModal is set to true, and a message is sent down the PassthroughSubject
The view, which is a subscriber of that subject, knows the model has changed and triggers a redraw.
If shouldPresentModal was set to true, additional UI drawing is executed.
I recommend watching this excellent WWDC 2019 talk:
Data Flow Through Swift UI
It makes all of the above clear.
I think you can do smth like that:
var body: some View {
VStack {
Text("Company ID")
}
.onAppear() {
self.loadContent()
}
}
private func loadContent() {
let url = URL(string: "https://your.url")!
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
DispatchQueue.main.async {
self.presentation(Modal(ContentView(), onDismiss: {
print("dismiss")
}))
}
}.resume()
}