KeyboardSceneDelegate: Animation styles were not empty on user driven presentation - ios

I'm having problem with my app. When I tap on a TextField, trying to fill data, in the sheet that just show up, my app freezes and four errors appear on console:
> [Assert] Animation styles expected to be empty on user driven
> presentation. Actually contains: (
> "<<_UIViewControllerKeyboardAnimationStyle: 0x281ee4d00>; animated = YES; duration = 0.35; force = NO>" )
>
> [KeyboardSceneDelegate] Animation styles were not empty on user driven
> presentation!
>
> Fit_Vein/ProfileView.swift:69: Fatal error: Unexpectedly found nil
> while unwrapping an Optional value
>
> Fit_Vein/ProfileView.swift:69: Fatal error: Unexpectedly found nil
> while unwrapping an Optional value
This situation takes place when I go from ProfileView to SettingsView, then try to do one of options: change email, password or delete account and input data in those sheets.
My code:
ProfileView:
struct ProfileView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#EnvironmentObject private var sessionStore: SessionStore
#Environment(\.colorScheme) var colorScheme
#State private var image = UIImage()
#State private var shouldPresentAddActionSheet = false
#State private var shouldPresentImagePicker = false
#State private var shouldPresentCamera = false
#State private var shouldPresentSettings = false
init(profileViewModel: ProfileViewModel) {
self.profileViewModel = profileViewModel
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
ScrollView(.vertical) {
HStack {
if profileViewModel.profilePicturePhotoURL != nil {
AsyncImage(url: profileViewModel.profilePicturePhotoURL!) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
.shadow(color: .gray, radius: 7)
.frame(width: screenWidth * 0.4, height: screenHeight * 0.2)
.onTapGesture {
self.shouldPresentAddActionSheet = true
}
} placeholder: {
Image(uiImage: UIImage(named: "blank-profile-hi")!)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
.shadow(color: .gray, radius: 7)
.frame(width: screenWidth * 0.4, height: screenHeight * 0.2)
}
} else {
Image(uiImage: UIImage(named: "blank-profile-hi")!)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
.shadow(color: .gray, radius: 7)
.frame(width: screenWidth * 0.4, height: screenHeight * 0.2)
.onTapGesture {
self.shouldPresentAddActionSheet = true
}
}
Spacer(minLength: screenWidth * 0.05)
VStack {
HStack {
Text(profileViewModel.profile!.firstName)
.foregroundColor(.green)
.font(.system(size: screenHeight * 0.03))
.fontWeight(.bold)
Spacer()
NavigationLink(destination: SettingsView(profile: profileViewModel).environmentObject(sessionStore), isActive: $shouldPresentSettings) {
Button(action: {
shouldPresentSettings = true
}, label: {
Image(systemName: "slider.vertical.3")
.resizable()
.aspectRatio(contentMode: .fit)
})
.frame(width: screenWidth * 0.12, height: screenHeight * 0.04)
.foregroundColor(colorScheme == .dark ? .white : .black)
}
}
.padding(.top, screenHeight * 0.02)
HStack {
Text(profileViewModel.profile!.username)
.foregroundColor(Color(uiColor: UIColor.lightGray))
Spacer()
}
Spacer()
}
}
.padding()
VStack {
Text("Level 1")
.font(.system(size: screenHeight * 0.03))
.fontWeight(.bold)
RoundedRectangle(cornerRadius: 25)
.frame(width: screenWidth * 0.9)
.padding()
.overlay(
HStack {
RoundedRectangle(cornerRadius: 25)
.foregroundColor(.green)
.padding()
.frame(width: screenWidth * 0.7)
Spacer()
}
)
.shadow(color: .gray, radius: 7)
Text("7 / 10 Workouts")
}
Spacer()
}
.sheet(isPresented: $shouldPresentImagePicker) {
ImagePicker(sourceType: self.shouldPresentCamera ? .camera : .photoLibrary, selectedImage: self.$image)
.onDisappear {
profileViewModel.uploadPhoto(image: image)
}
}
.actionSheet(isPresented: $shouldPresentAddActionSheet) {
ActionSheet(title: Text("Add a new photo"), message: nil, buttons: [
.default(Text("Take a new photo"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = true
}),
.default(Text("Upload a new photo"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = false
}),
ActionSheet.Button.cancel()
])
}
}
}
}
SettingsView:
import SwiftUI
struct SettingsView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#StateObject private var sheetManager = SheetManager()
#State private var shouldPresentActionSheet = false
#Environment(\.dismiss) var dismiss
private class SheetManager: ObservableObject {
enum Sheet {
case email
case password
case logout
case signout
}
#Published var showSheet = false
#Published var whichSheet: Sheet? = nil
}
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
Form {
Section(header: Text("Chats")) {
Toggle(isOn: .constant(false), label: {
Text("Hide my activity status")
})
}
Section(header: Text("Account")) {
Button(action: {
sheetManager.whichSheet = .email
sheetManager.showSheet.toggle()
}, label: {
Text("Change e-mail address")
})
Button(action: {
sheetManager.whichSheet = .password
sheetManager.showSheet.toggle()
}, label: {
Text("Change password")
})
Button(action: {
sheetManager.whichSheet = .logout
shouldPresentActionSheet = true
}, label: {
Text("Logout")
.foregroundColor(.red)
})
Button(action: {
sheetManager.whichSheet = .signout
shouldPresentActionSheet = true
}, label: {
Text("Delete account")
.foregroundColor(.red)
})
}
Section(header: Text("Additional")) {
Label("Follow me on GitHub:", systemImage: "link")
.font(.system(size: 17, weight: .semibold))
Link("#Vader20FF", destination: URL(string: "https://github.com/Vader20FF")!)
.font(.system(size: 17, weight: .semibold))
}
}
.navigationBarTitle("Settings")
.navigationBarTitleDisplayMode(.large)
.sheet(isPresented: $sheetManager.showSheet) {
switch sheetManager.whichSheet {
case .email:
ChangeEmailAddressSheetView(profile: profileViewModel)
case .password:
ChangePasswordSheetView(profile: profileViewModel)
case .signout:
DeleteAccountSheetView(profile: profileViewModel)
default:
Text("No view")
}
}
.confirmationDialog(sheetManager.whichSheet == .logout ? "Are you sure you want to logout?" : "Are you sure you want to delete your account? All data will be lost.", isPresented: $shouldPresentActionSheet, titleVisibility: .visible) {
if sheetManager.whichSheet == .logout {
Button("Logout", role: .destructive) {
profileViewModel.sessionStore!.signOut()
dismiss()
}
Button("Cancel", role: .cancel) {}
} else {
Button("Delete Account", role: .destructive) {
sheetManager.showSheet.toggle()
}
Button("Cancel", role: .cancel) {}
}
}
}
}
}
struct DeleteAccountSheetView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#Environment(\.dismiss) var dismiss
#State private var email = ""
#State private var password = ""
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
NavigationView {
VStack {
Form {
Section(footer: Text("Before you delete your account please provide your login credentials to confirm it is really you.")) {
TextField("E-mail", text: $email)
SecureField("Password", text: $password)
}
}
Button(action: {
withAnimation {
dismiss()
profileViewModel.deleteUserData() {
profileViewModel.sessionStore!.deleteUser(email: email, password: password) {
print("Successfully deleted user.")
}
}
}
}, label: {
Text("Delete account permanently")
})
.frame(width: screenWidth * 0.7, height: screenHeight * 0.08)
.background(Color.green)
.cornerRadius(15.0)
.font(.system(size: screenHeight * 0.026))
.foregroundColor(.white)
.padding()
}
.navigationBarHidden(true)
.ignoresSafeArea(.keyboard)
}
}
}
}
struct ChangeEmailAddressSheetView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#Environment(\.dismiss) var dismiss
#State private var oldEmail = ""
#State private var password = ""
#State private var newEmail = ""
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
NavigationView {
VStack {
Form {
Section(footer: Text("Before you change your e-mail address please provide your login credentials to confirm it is really you.")) {
TextField("Old e-mail address", text: $oldEmail)
SecureField("Password", text: $password)
TextField("New e-mail address", text: $newEmail)
}
}
Button(action: {
withAnimation {
dismiss()
profileViewModel.emailAddressChange(oldEmailAddress: oldEmail, password: password, newEmailAddress: newEmail) {}
}
}, label: {
Text("Change e-mail address")
})
.frame(width: screenWidth * 0.7, height: screenHeight * 0.08)
.background(Color.green)
.cornerRadius(15.0)
.font(.system(size: screenHeight * 0.026))
.foregroundColor(.white)
.padding()
}
.navigationBarHidden(true)
.ignoresSafeArea(.keyboard)
}
}
}
}
struct ChangePasswordSheetView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#Environment(\.dismiss) var dismiss
#State private var email = ""
#State private var oldPassword = ""
#State private var newPassword = ""
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
NavigationView {
VStack {
Form {
Section(footer: Text("Before you change your password please provide your login credentials to confirm it is really you.")) {
TextField("E-mail", text: $email)
SecureField("Old password", text: $oldPassword)
SecureField("New password", text: $newPassword)
}
}
Button(action: {
withAnimation {
dismiss()
profileViewModel.passwordChange(emailAddress: email, oldPassword: oldPassword, newPassword: newPassword) {}
}
}, label: {
Text("Change password")
})
.frame(width: screenWidth * 0.7, height: screenHeight * 0.08)
.background(Color.green)
.cornerRadius(15.0)
.font(.system(size: screenHeight * 0.026))
.foregroundColor(.white)
.padding()
}
.navigationBarHidden(true)
.ignoresSafeArea(.keyboard)
}
}
}
}
ProfileViewModel:
import Foundation
import SwiftUI
#MainActor
class ProfileViewModel: ObservableObject {
#Published var sessionStore: SessionStore?
private let firestoreManager = FirestoreManager()
private let firebaseStorageManager = FirebaseStorageManager()
#Published var profile: Profile?
#Published var profilePicturePhotoURL: URL?
#Published var fetchingData = true
init(forPreviews: Bool) {
self.profile = Profile(id: "sessionStore!.currentUser!.uid", firstName: "firstname", username: "username", birthDate: Date(), age: 18, country: "country", city: "city", language: "language", gender: "gender", email: "email", profilePictureURL: nil)
}
init() {
Task {
try await fetchData()
}
}
func setup(sessionStore: SessionStore) {
self.sessionStore = sessionStore
}
func fetchData() async throws {
if sessionStore != nil {
if sessionStore!.currentUser != nil {
print("Fetching Data")
fetchingData = true
let (firstname, username, birthDate, age, country, city, language, gender, email, profilePictureURL) = try await self.firestoreManager.fetchDataForProfileViewModel(userID: sessionStore!.currentUser!.uid)
self.profile = Profile(id: sessionStore!.currentUser!.uid, firstName: firstname, username: username, birthDate: birthDate, age: age, country: country, city: city, language: language, gender: gender, email: email, profilePictureURL: profilePictureURL)
if profilePictureURL != nil {
self.firebaseStorageManager.getDownloadURLForImage(stringURL: profilePictureURL!, userID: sessionStore!.currentUser!.uid) { photoURL in
self.profilePicturePhotoURL = photoURL
}
}
Task {
fetchingData = false
}
}
} else {
fetchingData = false
}
}
func uploadPhoto(image: UIImage) {
if self.profile!.profilePictureURL != nil {
Task {
try await self.firebaseStorageManager.deleteImageFromStorage(userPhotoURL: self.profile!.profilePictureURL!, userID: self.profile!.id)
}
}
self.firebaseStorageManager.uploadImageToStorage(image: image, userID: self.profile!.id) { photoURL in
self.firestoreManager.addProfilePictureURLToUsersData(photoURL: photoURL) {
Task {
try await self.fetchData()
}
}
}
}
func emailAddressChange(oldEmailAddress: String, password: String, newEmailAddress: String, completion: #escaping (() -> ())) {
self.sessionStore!.changeEmailAddress(oldEmailAddress: oldEmailAddress, password: password, newEmailAddress: newEmailAddress) {
print("Successfully changed user's e-mail address")
}
}
func passwordChange(emailAddress: String, oldPassword: String, newPassword: String, completion: #escaping (() -> ())) {
self.sessionStore!.changePassword(emailAddress: emailAddress, oldPassword: oldPassword, newPassword: newPassword) {
print("Successfully changed user's password")
}
}
func deleteUserData(completion: #escaping (() -> ())) {
if self.profile!.profilePictureURL != nil {
self.firestoreManager.deleteUserData(userUID: sessionStore!.currentUser!.uid) {
print("Successfully deleted user data")
Task {
try await self.firebaseStorageManager.deleteImageFromStorage(userPhotoURL: self.profile!.profilePictureURL!, userID: self.sessionStore!.currentUser!.uid)
completion()
}
}
}
}
}
ProfileModel:
import Foundation
import SwiftUI
struct Profile: Codable, Identifiable {
var id: String
var firstName: String
var username: String
var birthDate: Date
var age: Int
var country: String
var city: String
var language: String
var gender: String
var email: String
var profilePictureURL: String?
}

Related

SwiftUI NavigationLink doesn't trigger with onReceive method

I'm trying to learn how to do basic login in swift with Firebase and it's causing me to lose my mind over navigating to the main page of the app once the login is complete. I have a ViewModel that manages the login, and in the view I have added an onReceive property to listen on a viewModel's boolean to detect sign in and trigger the navigation. I don't understand why it's not working, any help would be greatly appreciated!
ViewModel:
class LoginViewModel: ObservableObject {
let auth = Auth.auth()
var userInfo: User?
#Published var isSignedIn = false
func signIn(email: String, password: String) {
auth.signIn(withEmail: email, password: password) { _, error in
if let error = error as? NSError {
print("Error happened on login"+error.description)
} else {
print("Login successful")
if let user = self.auth.currentUser {
print("We have a user")
self.userInfo = user
self.isSignedIn.toggle()
}
}
}
}
}
View:
struct LoginPage: View {
#State var email = ""
#State var password = ""
#ObservedObject var viewModel = LoginViewModel()
var body: some View {
NavigationView {
ZStack {
VStack {
TextField("Email", text: $email).padding()
.background(Color(.secondarySystemBackground))
.padding()
TextField("Password", text: $password).padding()
.background(Color(.secondarySystemBackground))
.padding()
Button(action: {
guard !email.isEmpty, !password.isEmpty else {
return
}
viewModel.signIn(email: email, password: password)
}, label: {
Text("Sign in")
.padding(EdgeInsets(top: 12, leading: 35, bottom: 12, trailing: 35))
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(15)
})
}
}.navigationTitle("Login")
}.onReceive(viewModel.$isSignedIn) { isSignedIn in
if isSignedIn {
print("ok damm")
NavigationLink(destination: HomePage()) {
EmptyView()
}
}
}
}
}
Note that "ok damm" prints every time.
Try the below code it will help you to solve your issue:
NavigationLink(destination: Text("Second View"), isActive: $isSignedIn){
EmptyView()
}
View:
struct LoginPage: View {
#State var email = ""
#State var password = ""
#ObservedObject var viewModel = LoginViewModel()
var body: some View {
NavigationView {
ZStack {
VStack {
TextField("Email", text: $email).padding()
.background(Color(.secondarySystemBackground))
.padding()
TextField("Password", text: $password).padding()
.background(Color(.secondarySystemBackground))
.padding()
Button(action: {
guard !email.isEmpty, !password.isEmpty else {
return
}
viewModel.signIn(email: email, password: password)
}, label: {
Text("Sign in")
.padding(EdgeInsets(top: 12, leading: 35, bottom: 12, trailing: 35))
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(15)
})
}
NavigationLink(destination: HomePage()), isActive: $isSignedIn){
EmptyView()
}
}.navigationTitle("Login")
}
}
}
Answer by Jatin Bhuva works but is deprecated for iOS 16. Here's the solution for new iOS versions:
struct LoginPage: View {
#State var email = ""
#State var password = ""
#ObservedObject var viewModel = LoginViewModel()
var body: some View {
NavigationStack {
ZStack {
VStack {
TextField("Email", text: $email).padding()
.background(Color(.secondarySystemBackground))
.padding()
TextField("Password", text: $password).padding()
.background(Color(.secondarySystemBackground))
.padding()
Button(action: {
guard !email.isEmpty, !password.isEmpty else {
return
}
viewModel.signIn(email: email, password: password)
}, label: {
Text("Sign in")
.padding(EdgeInsets(top: 12, leading: 35, bottom: 12, trailing: 35))
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(15)
})
Text("Don't have an account yet ?").padding(EdgeInsets(top: 20, leading: 0, bottom: 10, trailing: 0))
Button(action: {
guard !email.isEmpty, !password.isEmpty else {
return
}
viewModel.signUp(email: email, password: password)
}, label: {
Text("Create an account")
.padding(EdgeInsets(top: 12, leading: 35, bottom: 12, trailing: 35))
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(15)
})
}
}.navigationTitle("Login")
.navigationDestination(isPresented: $viewModel.isSignedIn) {
HomePage()
}
}
}
}
Notice how I replaced the NavigationView with a NavigationStack and added .navigationDestination(isPresented:) that listens for changes on my model's isSignedIn published property.
switch statement in swiftui can handle that easily. I use it all the time;
//Added code
enum LoginStages {
case preLogin, postLogin
}
class LoginViewModel: ObservableObject {
let auth = Auth.auth()
var userInfo: User?
#Published var loginStage: LoginStages = LoginStages.preLogin
//Added code
#Published var isSignedIn = false
func signIn(email: String, password: String) {
auth.signIn(withEmail: email, password: password) { _, error in
if let error = error as? NSError {
print("Error happened on login"+error.description)
} else {
print("Login successful")
if let user = self.auth.currentUser {
print("We have a user")
self.userInfo = user
self.isSignedIn.toggle()
loginStage = .postLogin //Added code
}
}
}
}
}
And your view should look like this;
struct LoginPage: View {
#State var email = ""
#State var password = ""
#ObservedObject var viewModel = LoginViewModel()
var body: some View {
switch viewModel.loginStage {
case .preLogin:
//This is ur initial code which the user sees before login.
// the same as the code you have above.
case .postLogin:
//Here, just call the post login view after a successful login;
// I am assuming it is called home view for this example.
HomeView()
}
}
}

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

Passing data from firebase to textfield

I'm having difficulties with the textfields. I'm fetching the user data from Firebase RTDB and i want to display some of the data in a profile view which contains textfields for the ability to edit the data.
My UserData:
struct UserData: Codable, Identifiable {
var email : String?
var name : String?
var firstname : String?
var lastname : String?
var type: String?
var uid: String?
var profileImageUrl : String?
var id : String?
var fcmToken2 : String?
var onboarding : Bool?
var phone : String?
}
The View with TextFields:
struct ProfileViewDemo: View {
#Binding var user : UserData
#State private var email: String = ""
var body: some View {
TextField("\(user.firstname!)", text: $email, onEditingChanged: { edit in
self.editing = edit
})
}
}
How do i preset the email var to the value of user.email? I tried using .onAppear, but that caused problem with the photo loading. Are there any ways to preset the email var with the data from UserData? Thanks.
EDIT (Adding the ProfileView code)
struct ProfileViewDemo: View {
#ObservedObject var session : SessionStore
#Binding var user : UserData
#State private var number: String = ""
#Binding var email: String = "123"
#State private var editing = false
#State private var editing2 = false
#State private var editing3 = false
#State private var editing4 = false
#State var refresh: Bool = false
#State private var image: UIImage?
#State private var shouldPresentImagePicker = false
#State private var shouldPresentActionScheet = false
#State private var shouldPresentCamera = false
var body: some View {
ScrollView{
VStack{
VStack{
ZStack{
ZStack{
if(image == nil){
KFImage.url(URL(string: "\(user.profileImageUrl!)"))
.loadDiskFileSynchronously()
.cacheMemoryOnly()
.fade(duration: 0.25)
.onProgress { receivedSize, totalSize in }
.onSuccess { result in }
.onFailure { error in }
.resizable()
.aspectRatio(contentMode: .fill)
.clipShape(Circle())
.overlay(Circle().stroke(Color(red: 50 / 255, green: 51 / 255, blue: 53 / 255), lineWidth: 2))
.frame(width: 140, height: 140)
}
if (image != nil) {
Image(uiImage: image!)
.resizable()
.aspectRatio(contentMode: .fill)
.clipShape(Circle())
.overlay(Circle().stroke(Color(red: 50 / 255, green: 51 / 255, blue: 53 / 255), lineWidth: 2))
.frame(width: 140, height: 140)
}
}
ZStack{
Button {
self.shouldPresentActionScheet = true
} label: {
Image(systemName: "camera")
.font(.system(size: 12))
.padding(.all, 5)
.background(Color(red: 50 / 255, green: 51 / 255, blue: 53 / 255))
.foregroundColor(Color.white)
.clipShape(Circle())
}
.sheet(isPresented: $shouldPresentImagePicker) {
SUImagePickerView(sourceType: self.shouldPresentCamera ? .camera : .photoLibrary, image: self.$image, isPresented: self.$shouldPresentImagePicker)
}.actionSheet(isPresented: $shouldPresentActionScheet) { () -> ActionSheet in
ActionSheet(title: Text("Choose mode"), message: Text("Please choose your preferred mode to set your profile image"), buttons: [ActionSheet.Button.default(Text("Camera"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = true
}), ActionSheet.Button.default(Text("Photo Library"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = false
}), ActionSheet.Button.cancel(Text("Atšaukti"))])
}
}
.padding(.all, 3)
.background(Color.white)
.clipShape(Circle())
.offset(x: 50)
.offset(y: 50)
}
}
.padding(.bottom, 15)
ZStack {
ZStack(alignment: .leading){
ZStack(alignment: .leading){
TextField("\(user.email!)", text: $email, onEditingChanged: { edit in
self.editing3 = edit
})
.padding(.leading, 5)
.padding(.trailing, 5)
.padding()
.offset(y: 0)
}.frame(maxWidth: .infinity, maxHeight: 50)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(editing3 ? Color(UIColor.systemGray2) : Color(UIColor.systemGray5), lineWidth: 2)
)
.background(Color(UIColor.white))
.cornerRadius(5)
Text("El. Pašto adresas")
.disableAutocorrection(true)
.padding(.leading, 5)
.padding(.trailing, 5)
.background(Color.white)
.font(Font.custom("Montserrat-Regular", size: 15.0))
.foregroundColor(Color.gray)
.padding()
.offset(y: -25)
}
.frame(maxWidth: .infinity, maxHeight: 50)
}
.padding(.bottom, 15)
}
.padding()
.navigationBarItems(trailing:
Button("Išsaugoti") {
print("Presed Profilio Informacija išsaugota!")
updateProfileInfo()
UIApplication.shared.endEditing()
})
}
.onTapGesture {
self.endTextEditing()
}
}
func updateProfileInfo () {
if (image != nil) {
session.uplaodImage(image!) { (url) in
if let url = url {
session.updateProfileImage(url: url)
print("Profile Image updated")
}
}
}
if (image == nil){
print("Skipping image update - empty")
}
refresh.toggle()
}
}
Code to fetch UserData:
func fetchUsers(){
ref.child("users").observe(.childAdded) { (snapshot) in
guard let dictionary = snapshot.value as? [String: AnyObject] else { return}
var user = UserData()
user.email = (dictionary["email"] as! String)
user.name = (dictionary["name"] as! String)
user.firstname = (dictionary["firstname"] as! String)
user.lastname = (dictionary["lastname"] as! String)
user.type = (dictionary["type"] as! String)
user.uid = (dictionary["uid"] as! String)
user.profileImageUrl = (dictionary["profileImageUrl"] as! String)
user.id = snapshot.key
user.fcmToken2 = (dictionary["fcmToken"] as! String)
user.phone = (dictionary["phone"] as! String)
user.onboarding = (dictionary["onboarding"] as! Bool)
self.users.append(user)
}
}
Use OnAppear function on the View and assign values or Fire your API call from there.
struct ProfileViewDemo: View {
#Binding var user : UserData
#Binding var email: String = ""
var body: some View {
if email == "" {
TextField("Enter email", text: $email)
}
else {
TextField("", text: $email)
}
}.onAppear(perform: {
email = user.email
//You can fire your API call in here and assign the values
}
}

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.

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

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

Resources