How To Resolve a "Thread 1: Fatal error: No ObservableObject of type,,," Crash Message - fatal-error

I am a total rookie to coding, and this is my first project. I have created an app that determines the risk of CardioVascular Disease(CVD) based on the data from cholesterol panel lab tests.The app runs on the iPhone XR (canvas simulator) with no trouble. However, when i run the app on my actual iPhone XR device, it crashes and yields the following message:
Fatal Error: No ObservableObject of Type Data found. A View.environmentObject(_:) for Data
may be missing as an ancestor of this view.
Here is where the error appeared:
TextField("", text: $data.name)
.frame(width:350, height: 40)
.padding(.leading)
.background(Color.offWhite)
.foregroundColor(.fblue)
.cornerRadius(5)
.shadow(color: Color.black.opacity(0.4), radius: 5, x: 5, y: 5)
.shadow(color: Color.white.opacity(1.5), radius:5, x: -5, y: -5)
.font(.title)
.keyboardType(.alphabet)
I have searched Stack Overflow (SO) and attempted all of the recommended solutions, none of which worked for me. I also searched outside of SO and made similar attempts with similar results. I have returned to SO to see if anyone can help me. Based on this research, I think I'm having a problem with making my ObservableObject "visible" to the entire app even though I've precisely followed well-respected coders and coding gurus (SO contributors, Paul Hudson, AzamSharp, Maxcodes, Brian Advent, Code with Chris etc;) and their analogous examples during the building of my app. So, my problem is either in the syntax of the code I've used to reference the ObservableObject, or I've placed that reference code in the wrong place within the app, or there's a piece of code that I'm missing, and it was never referred to in any of my research.
The app consists of three views of user input utilizing TextFields, NavViews and NavLinks. One view showing the results of user input, and a final view showing a diagnosis and % risk of CVD. I employed #EnvironmentObject, ObservableObject, #Published var, and #State private var throughout.
Here are some code details:(only pertinent code is presented)
My Scene Delegate:
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene (_: UIScene, WillConnectTo session: UISceneSession, option connectingOptions:
UIScene.ConnectionOptions) {
//......
//......
let data = Data()
//....
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
// .....
// .....
let contentView = ContentView(
.environmentObject(data)
.environment(\.managedObjectContext, context)
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(data))
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect.......
My class declaration in ContentView:
class Data: ObservableObject {
#Published var name = ""
#Published var age = ""
#Published var dob = ""
#Published var gender = ""
#Published var fast = ""
#Published var tc = ""
#Published var tg = ""
#Published var hdl = ""
#Published var ldl = ""
#Published var vldl = ""
#Published var apoB = ""
#Published var apoA1 = ""
#Published var apoRatio = ""
}
My ResultsDiagnostics struct in ResultsDiagnosticsView:
struct ResultsDiagnosticsView: View {
init() {
UITableView.appearance().tableFooterView = UIView()
UITableView.appearance().separatorStyle = .none
}
#EnvironmentObject var data: Data
#State private var name = ""
#State private var age = ""
#State private var dob = ""
#State private var selectedGender = ""
#State private var tc = ""
#State private var tg = ""
#State private var hdl = ""
#State private var ldl = ""
#State private var vldl = ""
#State private var apoB = ""
#State private var apoA1 = ""
var ptDOB: Double{
let patientDOB = Double(data.dob) ?? 0
return patientDOB
}
var ptTC: Double{
let patientTC = Double(data.tc) ?? 0
return patientTC
}
var ptTG: Double{
let patientTG = Double(data.tg) ?? 0
return patientTG
}
var ptHDL: Double{
let patientHDL = Double(data.hdl) ?? 0
return patientHDL
}
var ptLDL: Double{
let patientLDL = Double(data.ldl) ?? 0
return patientLDL
}
var ptVLDL: Double{
let patientVLDL = Double(data.vldl) ?? 0
return patientVLDL
}
var ptAPOB: Double{
let patientAPOB = Double(data.apoB) ?? 0
return patientAPOB
}
var ptAPOA1: Double{
let patientAPOA1 = Double(data.apoA1) ?? 0
return patientAPOA1
}
var ptAPOBA1: Double{
let patientAPOBA1 = ptAPOB/ptAPOA1
return patientAPOBA1
}
My DiagnosticsRisk stuct DiagnosticsRiskView:
struct DiagnosticsRiskView: View {
#EnvironmentObject var data: Data
#State private var name = ""
#State private var age = ""
#State private var dob = ""
#State private var selectedGender = ""
#State private var tc = ""
#State private var tg = ""
#State private var hdl = ""
#State private var ldl = ""
#State private var vldl = ""
#State private var apoB = ""
#State private var apoA1 = ""
var ptDOB: Double{
let patientDOB = Double(data.dob) ?? 0
return patientDOB
}
var ptTC: Double{
let patientTC = Double(data.tc) ?? 0
return patientTC
}
var ptTG: Double{
let patientTG = Double(data.tg) ?? 0
return patientTG
}
var ptHDL: Double{
let patientHDL = Double(data.hdl) ?? 0
return patientHDL
}
var ptLDL: Double{
let patientLDL = Double(data.ldl) ?? 0
return patientLDL
}
var ptVLDL: Double{
let patientVLDL = Double(data.vldl) ?? 0
return patientVLDL
}
var ptAPOB: Double{
let patientAPOB = Double(data.apoB) ?? 0
return patientAPOB
}
var ptAPOA1: Double{
let patientAPOA1 = Double(data.apoA1) ?? 0
return patientAPOA1
}
var ptAPOBA1: Double{
let patientAPOBA1 = ptAPOB/ptAPOA1
return patientAPOBA1
}
func progress() -> Double {
let j = (ptAPOBA1*3.43) - 0.099
let k = (j/(1+j)) * 100
let l = Double(round(10*k)/10)
return l
}
func setProgress()->CGFloat{
let temp = self.progress() / 2
return CGFloat(temp * 0.01)
}
My NavView?NavLink for ResultsDiagnosticsView:
NavigationView {
VStack {
List{
Section(header: Text("Demographics").modifier(SectionMod())) {
HStack {
Text("Patient")
.modifier(Label())
Text(data.name)
.modifier(ResultText())
}.modifier(HStackMod())
.padding(.top, 10)
HStack {
HStack{
Text("Gender")
.modifier(Label())
Text(data.gender)
.modifier(ResultText())
}.modifier(HStackMod())
HStack{
Text("Fasting")
.modifier(Label())
Text(data.fast)
.modifier(ResultText())
}.modifier(HStackMod())
}
HStack {
Text("DOB ")
.modifier(Label())
Text(data.dob)
.modifier(ResultText())
}.modifier(HStackMod())
.padding(.bottom, 10)
}
Section(header: Text("Cholesterol/Triglycerides").modifier(SectionMod())) {
HStack{
Text("Cholesterol")
.modifier(Label())
Spacer()
.frame(width:115)
if ptTC < 200 {
Text("\(ptTC, specifier: "%.2f") N")
.modifier(Normal())
}else{
Text("\(ptTC, specifier: "%.2f") H")
.modifier(Abnormal())
}
}.modifier(HStackMod())
.padding(.top)
HStack{
Text("Triglycerides")
.modifier(Label())
Spacer()
.frame(width:115)
if ptTG < 100 {
Text("\(ptTG, specifier: "%.2f") N")
.modifier(Normal())
}else{
Text("\(ptTG, specifier: "%.2f") H")
.modifier(Abnormal())
}
}.modifier(HStackMod())
.padding(.bottom, 10)
}
Section(header: Text("Lipoproteins").modifier(SectionMod())) {
HStack{
Text("HDL")
.modifier(Label())
Spacer()
if ptHDL >= 50{
Text("\(ptHDL, specifier: "%.2f") N")
.modifier(Normal())
}else{
Text("\(ptHDL, specifier: "%.2f") L")
.modifier(Abnormal())
}
}.modifier(HStackMod())
.padding(.top, 10)
HStack{
Text("LDL")
.modifier(Label())
Spacer()
if ptLDL < 100{
Text("\(ptLDL, specifier: "%.2f") N")
.modifier(Normal())
}else{
Text("\(ptLDL, specifier: "%.2f") H")
.modifier(Abnormal())
}
}.modifier(HStackMod())
HStack{
Text("VLDL")
.modifier(Label())
Spacer()
if ptVLDL <= 30{
Text("\(ptVLDL, specifier: "%.2f") N")
.modifier(Normal())
}else{
Text("\(ptVLDL, specifier: "%.2f") H")
.modifier(Abnormal())
}
}.modifier(HStackMod())
.padding(.bottom, 10)
}
Section(header: Text("Apolipoproteins").modifier(SectionMod())) {
HStack{
Text("Apo B")
.modifier(Label())
Spacer()
if ptAPOB < 85 {
Text("\(ptAPOB, specifier: "%.2f") N")
.modifier(Normal())
}else{
Text("\(ptAPOB, specifier: "%.2f") H")
.modifier(Abnormal())
}
}.modifier(HStackMod())
.padding(.top)
HStack {
Text("Apo A1")
.modifier(Label())
Spacer()
if ptAPOA1 >= 135{
Text("\(ptAPOA1, specifier: "%.2f") N")
.modifier(Normal())
}else{
Text("\(ptAPOA1, specifier: "%.2f") L")
.modifier(Abnormal())
}
}.modifier(HStackMod())
HStack {
Text("Apo Ratio")
.modifier(Label())
Spacer()
if ptAPOBA1 < 0.63 {
Text("\(ptAPOBA1, specifier: "%.2f") N")
.modifier(Normal())
}else{
Text("\(ptAPOBA1, specifier: "%.2f ") H")
.modifier(Abnormal())
}
}.modifier(HStackMod())
}
}
HStack{
NavigationLink(destination: TCTGView().navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)) {
Text("Start Over")
.fontWeight(.bold)
.font(.subheadline)
.padding()
.background(Color.fblue)//
.foregroundColor(.white)
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.4), radius: 5, x: 5, y: 5)
.shadow(color: Color.white.opacity(1.5), radius:5, x: -5, y: -5)
}
NavigationLink(destination: ApolipoproteinsView().navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)) {
Text("Back")
.fontWeight(.bold)
.font(.subheadline)
.padding()
.background(Color.fblue)
.foregroundColor(.white)
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.4), radius: 5, x: 5, y: 5)
.shadow(color: Color.white.opacity(1.5), radius:5, x: -5, y: -5)
}
NavigationLink(destination: DiagnosticsRiskView().navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)) {
Text("Next")
.fontWeight(.bold)
.font(.subheadline)
.padding()
.background(Color.fblue)
.foregroundColor(.white)
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.4), radius: 5, x: 5, y: 5)
.shadow(color: Color.white.opacity(1.5), radius:5, x: -5, y: -5)
}
}.padding(.bottom, 80)
}
}.edgesIgnoringSafeArea(.top)
If a full copy of the app would be more helpful, please email me (wcmarrocco#gmail.com), since I couldn't figure out how to embed a copy of my project here.Thank you!

In your ResultsDiagnosticsView add the ObservalObject at the top and then push it to your NavigationsLinks manually by adding...
NavigationLink(destination: TCTGView().environmentObject(self.data)

Related

How to show random array's items only once in SwiftUI?

I'm trying to recreate a popular game: Heads up, basically the user has to try to guess the name putting the phone on his head, with friends' suggestions...if he raises the head he skips the word, if he lowers the head it means he guessed the name and he earns a point. He has limited time. I need that every time the user raises/lowers his head, the array's name changes, and each name must appear only once. Any suggestions?
This is my code:
import SwiftUI
import CoreMotion
struct ContentView: View {
let motionManager = CMMotionManager()
let queue = OperationQueue()
#State private var roll = Double.zero
#State private var people = ["John", "Marcus", "Steve", "Eric", "Philip"].shuffled()
#State private var randomPerson = Int.random(in: 0...4)
let timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()
#State private var timeRemaining = 10
#State private var score = 0
var body: some View {
NavigationView {
ZStack {
//Show a red background and "SKIP" if the user raises head
if roll < 1 {
Color.red
.ignoresSafeArea()
Text("SKIP")
.font(.largeTitle)
.bold()
.foregroundColor(.white)
} else if roll > 2.1 {
//Show a green background and "CORRECT" if user lowers head
Color.green
.ignoresSafeArea()
Text("CORRECT")
.font(.largeTitle)
.bold()
.foregroundColor(.white)
.onAppear {
score += 1
}
} else {
//Otherwise show a cyan back with array's name
Color.cyan
.ignoresSafeArea()
Text(people[randomPerson])
.font(.largeTitle)
.bold()
.foregroundColor(.white)
}
Text("\(timeRemaining)")
.font(.system(size: 39))
.padding(.bottom, 200)
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
Text("Score: \(score)")
.font(.largeTitle)
.bold()
.foregroundColor(.white)
.padding(.top, 200)
}
.onAppear {
//Detect device motion
self.motionManager.startDeviceMotionUpdates(to: self.queue) { (data: CMDeviceMotion?, error: Error?) in
guard let data = data else {
print("Error: \(error!)")
return
}
let attitude: CMAttitude = data.attitude
DispatchQueue.main.async {
self.roll = attitude.roll
}
}
}
}
.navigationViewStyle(.stack)
}
}
You can do like this:
a state variable for current selected person
#State private var currerntPerson : String = ""
a function to get random person
getRandomPerson()
change TextView show selected person name:
Text(currerntPerson)
.font(.largeTitle)
.bold()
.foregroundColor(.white)
.onAppear {
getRandomPerson()
}
====
All code here:
let motionManager = CMMotionManager()
let queue = OperationQueue()
#State private var roll = Double.zero
#State private var people = ["John", "Marcus", "Steve", "Eric", "Philip"].shuffled()
#State private var randomPerson = Int.random(in: 0...4)
let timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()
#State private var timeRemaining = 10
#State private var score = 0
#State private var currerntPerson : String = ""
var body: some View {
NavigationView {
ZStack {
//Show a red background and "SKIP" if the user raises head
if roll < 1 {
Color.red
.ignoresSafeArea()
Text("SKIP")
.font(.largeTitle)
.bold()
.foregroundColor(.white)
} else if roll > 2.1 {
//Show a green background and "CORRECT" if user lowers head
Color.green
.ignoresSafeArea()
Text("CORRECT")
.font(.largeTitle)
.bold()
.foregroundColor(.white)
.onAppear {
score += 1
}
} else {
//Otherwise show a cyan back with array's name
Color.cyan
.ignoresSafeArea()
Text(currerntPerson)
.font(.largeTitle)
.bold()
.foregroundColor(.white)
.onAppear {
getRandomPerson()
}
}
Text("\(timeRemaining)")
.font(.system(size: 39))
.padding(.bottom, 200)
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
Text("Score: \(score)")
.font(.largeTitle)
.bold()
.foregroundColor(.white)
.padding(.top, 200)
}
.onAppear {
//Detect device motion
self.motionManager.startDeviceMotionUpdates(to: self.queue) { (data: CMDeviceMotion?, error: Error?) in
guard let data = data else {
print("Error: \(error!)")
return
}
let attitude: CMAttitude = data.attitude
DispatchQueue.main.async {
self.roll = attitude.roll
}
}
}
}
.navigationViewStyle(.stack)
}
func getRandomPerson() {
if people.count > 0 {
let index = Int.random(in: 0..<people.count)
currerntPerson = people[index]
people.remove(at: index)
}
}

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

Swift UI | Textfield not reading entered value

I have a textfield which is supposed to log the units of a food product someone has eaten, which is then used to calculate the total number of calories, protein, etc. that the user consumed. But when the value is entered on the textfield, the units variable isn't updated. How can I fix this?
This is my code:
#State var selectFood = 0
#State var units = 0
#State var quantity = 1.0
#State var caloriesInput = 0.0
#State var proteinInput = 0.0
#State var carbsInput = 0.0
#State var fatsInput = 0.0
var body: some View {
VStack {
Group {
Picker(selection: $selectFood, label: Text("What did you eat?")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.white))
{
ForEach(database.productList.indices, id: \.self) { i in
Text(database.productList[i].name)
}
}
.pickerStyle(MenuPickerStyle())
Spacer(minLength: 25)
Text("How much did you have?")
.font(.headline)
.fontWeight(.bold)
.foregroundColor(.white)
.frame(alignment: .leading)
//Textfield not working.
TextField("Units", value: $units, formatter: NumberFormatter())
.padding(10)
.background(Color("Settings"))
.cornerRadius(10)
.foregroundColor(Color("Background"))
.keyboardType(.numberPad)
Button (action: {
self.quantity = ((database.productList[selectFood].weight) * Double(self.units)) / 100
caloriesInput = database.productList[selectFood].calories * quantity
proteinInput = database.productList[selectFood].protein * quantity
carbsInput = database.productList[selectFood].carbs * quantity
fatsInput = database.productList[selectFood].fats * quantity
UIApplication.shared.hideKeyboard()
}) {
ZStack {
Rectangle()
.frame(width: 90, height: 40, alignment: .center)
.background(Color(.black))
.opacity(0.20)
.cornerRadius(15)
;
Text("Enter")
.foregroundColor(.white)
.fontWeight(.bold)
}
}
}
}
}
This is an issue with NumberFormatter that has been going on for a while. If you remove the formatter it updates correctly.
This is a workaround. Sadly it requires 2 variables.
import SwiftUI
struct TFConnection: View {
#State var unitsD: Double = 0
#State var unitsS = ""
var body: some View {
VStack{
//value does not get extracted properly
TextField("units", text: Binding<String>(
get: { unitsS },
set: {
if let value = NumberFormatter().number(from: $0) {
print("valid value")
self.unitsD = value.doubleValue
}else{
unitsS = $0
//Remove the invalid character it is not flawless the user can move to in-between the string
unitsS.removeLast()
print(unitsS)
}
}))
Button("enter"){
print("enter action")
print(unitsD.description)
}
}
}
}
struct TFConnection_Previews: PreviewProvider {
static var previews: some View {
TFConnection()
}
}

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)

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