Continuing on my learning path of SwiftUI, I stumble on what seems like a simple problem, but not to me !
I am trying to display a Text in a colored border. The color depends on a condition that I set inside a func.
I declared a var Bool in order to keep track of that condition, but when I try to write to it, I get the dreaded Cannot assign to property: 'self' is immutable error.
If I declare that Bool using the #State wrapper, I then get the other dreaded Modifying state during view update, this will cause undefined behavior message.
If anyone can help, would be [again] much appreciated.
Here is a summary of my code, simplified for clarity :
import SwiftUI
import CoreData
struct ContentView: View {
var isCurrent = false
var body: some View {
VStack {
Spacer()
Text(minimumEventsCheck().0) // Currency string
.lineLimit(nil) // allows unlimited lines
.padding(.all)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(isCurrent ? Color.green : Color.red, lineWidth: 1))
Text(minimumEventsCheck().1) // details string
.lineLimit(nil) // allows unlimited lines
.padding(.all)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(isCurrent ? Color.green : Color.red, lineWidth: 1))
Spacer()
} // END of main VStack
} // END of body
private func minimumEventsCheck() -> (String, String) {
var aString = ""
var bString = ""
let refDate = Date ()
let date90Prior = refDate.addingTimeInterval(-7776000)
if date90Prior < refDate { // This is always true, but it's for demo only
aString = "True"
bString = "Also true"
isCurrent = true // TRIGGERS Xcode error : Cannot assign to property: 'self' is immutable
} else {
aString = "False"
bString = "Also false"
isCurrent = false // TRIGGERS Xcode error : Cannot assign to property: 'self' is immutable
}
return (aString, bString)
} // End of func currencyText() -> String
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return ContentView().environment(\.managedObjectContext, context)
}
}
use
#State var isCurrent = false
The view is a struct and structs are by default immutable. Therefore SwiftUI has propertywrapper #State to change the struct/variables.
I see two possible purpose of trying to do that.
First is to check some date once one start. You can do that in init and store the result in a struct property like this:
struct ContentView: View {
var isCurrent = false
var aString: String
var bString: String
init(refDate: Date){
if refDate < Date().addingTimeInterval(-7776000) {
self.aString = "True"
self.bString = "Also true"
self.isCurrent = true
} else {
self.aString = "False"
self.bString = "Also false"
self.isCurrent = false
}
}
var body: some View {
VStack {
Spacer()
Text(self.aString) // Currency string
.lineLimit(nil) // allows unlimited lines
.padding(.all)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(isCurrent ? Color.green : Color.red, lineWidth: 1))
Text(self.bString) // details string
.lineLimit(nil) // allows unlimited lines
.padding(.all)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(isCurrent ? Color.green : Color.red, lineWidth: 1))
Spacer()
} // END of main VStack
}
}
In a parent View (or in sceneDelegate file if it is the root view) you will have to pass this date like this:
ContentView(refDate: Date())
Then, when you change the date outside of the View, this view will be triggered to initiate with new values, and the dates will be checked again. This is SwiftUI way of coding.
If you trying to make some kind of function that will updates this view from the outside, you will have to use a passthroughSubject when changing something, and catch notification in .onReceive modifier. This is the only way of updating values inside the View without it being initialize again.
OK, Got this to work the following way :
Got rid of the #State var Bool in my View struct.
Modified the func to return the tuple (String, String, Bool) iso ((String, String) and called minimumEventsCheck().2 to access it in code :
import SwiftUI
import CoreData
struct ContentView: View {
var body: some View {
VStack {
Spacer()
Text(minimumEventsCheck().0) // Currency string
.lineLimit(nil) // allows unlimited lines
.padding(.all)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(minimumEventsCheck().2 ? Color.green : Color.red, lineWidth: 1))
Text(minimumEventsCheck().1) // details string
.lineLimit(nil) // allows unlimited lines
.padding(.all)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(minimumEventsCheck().2 ? Color.green : Color.red, lineWidth: 1))
Spacer()
} // END of main VStack
} // END of body
private func minimumEventsCheck() -> (String, String, Bool) {
var isCurrent = false
var aString = ""
var bString = ""
let refDate = Date ()
let date90Prior = refDate.addingTimeInterval(-7776000)
if date90Prior < refDate { // This is always true, but it's for demo only
aString = "True"
bString = "Also true"
isCurrent = true
} else {
aString = "False"
bString = "Also false"
isCurrent = false
}
return (aString, bString, isCurrent)
} // End of func currencyText() -> String
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return ContentView().environment(\.managedObjectContext, context)
}
}
Related
I'm trying to make real time email validation, but the simulator has strange behaviour. When the email IS NOT valid, it should be red, when it IS valid, the color of the text should be black, but some characters stay red.
struct EmailText: View {
#State var textFieldValue: String = ""
private func isValid(_ s: String) -> Bool {
// not real validation function, just for simplicity
return Int.random(in: 0 ... 1) == 1
}
var body: some View {
TextField("", text: $textFieldValue)
.foregroundColor(isValid(textFieldValue) ? .black : .red)
.padding()
}
}
Edit: I have updated the validation function from regex to simple condition, to exclude regex as a possible issue.
Solution: disable autocorrection.
TextField("", text: $textFieldValue)
.foregroundColor(isValid(textFieldValue) ? .black : .red)
.padding()
.autocorrectionDisabled() // <----
Most certainly a SwiftUI bug that causes interference between the text highlighting of spell check warnings and the foreground color.
Confirmed. Your RegEx is wrong. Use:
/^\w+([\.-]?\w+)*#\w+([\.-]?\w+)*(\.\w{2,3})+$/
To validate e-mail.
Here I have tested code, Maybe It will help you
struct Demo1: View {
#State private var email: String
#State var emailIsValid: Bool = true
public init(email: String = "")
{
self.email = email
}
var body: some View {
// Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
TextField("Email", text: $email)
.onChange(of: email) { newValue in
if(newValue.range(of:"^\\w+([-+.']\\w+)*#\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$", options: .regularExpression) != nil) {
self.emailIsValid = true
print("valid")
} else {
self.emailIsValid = false
print("invalid")
}
}
.foregroundColor(emailIsValid ? Color.green : Color.red)
}
}
I have a view that outputs rows of data using ForEach. The data is an array of defined data records from an FMDB database. I use List / ForEach and LazyVGrid to populate the rows in the view.
My goal is to take two of the output fields in the LazyVGrid and use onGesture to invoke one of two .sheet views for the details behind the value.
In the onGesture I want to capture the dataRecord used to populate that row and use the data in that record to call the view in .sheet
It all seems to work until I get to the .sheet(isPresented...
There the #State variable I populate in the onGesture is Nil
I'm putting in the code I use along with some print output that seems to show that the #State variable is populated in onGesture, but later in the .sheet(isPresented it is nil
I'm not sure I understand the scope of visibility for that #State variable and wonder if someone can help me figure this out...
MY CODE IS:
import SwiftUI
struct BudgetedIncomeView: View {
#State var account_code: Int
#State var budgetYear: Int
#State var budgetMonth: Int
#State private var isAdding = false
#State private var isEditing = false
#State private var isDeleting = false
#State private var budgetRec = BudgetedIncome.BudgetedIncomeRecord()
#State private var isBudgeted = false
#State private var isReceived = false
// Environment and ObservedObjects
#Environment(\.presentationMode) var presentationMode
#Environment(\.editMode) var editMode
#StateObject var toolbarViewModel = ToolbarViewModel()
var displayMonthYearString: String {
return calendarMonths(budgetMonth) + "-" + String(budgetYear)
}
let columns = [
GridItem(.flexible(), alignment: .leading),
GridItem(.fixed(UIScreen.main.bounds.width * 0.20), alignment: .trailing),
GridItem(.fixed(UIScreen.main.bounds.width * 0.20), alignment: .trailing)
]
let numberColor = Color.black
let negativeColor = Color.red
// Array of Budgeted Income records for View List
var budgetedIncome: [BudgetedIncome.BudgetedIncomeRecord] {
return BudgetedIncome.shared.selectBudgetedIncomeForAccountWithMonthAndYear(withAccountCode: self.account_code, withYear: self.budgetYear, withMonth: self.budgetMonth)
}
var body: some View {
ZStack {
VStack {
BudgetedIncomeHeader(headerText: "Budgeted")
.padding(.top, 10)
List {
ForEach (self.budgetedIncome, id: \.self) { budgetRecord in
LazyVGrid(columns: columns, spacing: 5) {
Text("\(budgetRecord.description)")
.lineLimit(1)
.padding(.leading, 5)
Text("\(NumberFormatter.formatWithComma(value: budgetRecord.income_budget))")
.foregroundColor((budgetRecord.income_budget < 0 ) ? self.negativeColor : self.numberColor)
.onTapGesture {
//
// PRINT STATEMENTS THAT SHOW THE DATA IS CAPTURED
//
let _ = print("budgetRecord in onGesture = \(budgetRecord)\n\n")
budgetRec = budgetRecord
let _ = print("budgetRec in onGesture = \(budgetRec)\n\n")
isBudgeted.toggle()
}
Text("\(NumberFormatter.formatWithComma(value: budgetRecord.income_received))")
.underline()
.padding(.trailing, 15)
}// END OF LAZYVGRID
} // END OF FOREACH
} // END OF LIST
BudgetedIncomeFooter(accountCode: $account_code, budgetYear: $budgetYear, budgetMonth: $budgetMonth)
.padding(.top, 5)
} // END OF VSTACK
.sheet(isPresented: $isBudgeted ){
//
// PRINT STATEMENT THAT SHOWS THE DATA IS NIL HERE
//
let _ = print("budgetRec in .sheet = \(budgetRec)\n\n")
BudgetedIncomeDetailsView(accountCode: budgetRec.account_code, incomeCode: budgetRec.income_code, budgetYear: budgetRec.budget_year, budgetMonth: budgetRec.budget_month)
.opacity(isBudgeted ? 1 : 0)
.zIndex(isBudgeted ? 1 : 0)
}
if isReceived {
BudgetedIncomeDetailsView(accountCode: 12345678, incomeCode: 50060, budgetYear: 2020, budgetMonth: 12)
.opacity(isReceived ? 1 : 0)
.zIndex(isReceived ? 1 : 0)
}
} // END OF ZSTACK
.navigationTitle("Budgeted Income for \(self.displayMonthYearString)")
.navigationBarBackButtonHidden(true)
.toolbar {
ToolBarCancelDeleteAdd() {
toolbarViewModel.cancelContent(editMode: editMode)
presentationMode.wrappedValue.dismiss()
}
adding: {
toolbarViewModel.addingContent(isAdding: &isAdding, editMode: editMode)
}
} // END OF TOOLBAR
} // END OF BODY VIEW
} // END OF STRUCT VIEW
struct BudgetedIncomeView_Previews: PreviewProvider {
static var previews: some View {
BudgetedIncomeView(account_code: 12345678,
budgetYear: 2020,
budgetMonth: 12)
.environmentObject(ApplicationSettings())
.environmentObject(Budget())
.environmentObject(GlobalSettings())
}
}
The output from these print statements is:
budgetRecord in onGesture = BudgetedIncomeRecord(income_id: Optional(589), account_code: Optional(12345678), income_code: Optional(50060), budget_year: Optional(2020), budget_month: Optional(12), description: Optional("ADD BACK SET ASIDE"), category: Optional("*Exclude From Reports"), income_budget: Optional(3600.0), income_received: Optional(3600.0), unexpected_income: Optional(0.0), category_code: Optional(99999), set_aside: Optional(true), set_aside_id: nil)
budgetRec in onGesture = BudgetedIncomeRecord(income_id: Optional(589), account_code: Optional(12345678), income_code: Optional(50060), budget_year: Optional(2020), budget_month: Optional(12), description: Optional("ADD BACK SET ASIDE"), category: Optional("*Exclude From Reports"), income_budget: Optional(3600.0), income_received: Optional(3600.0), unexpected_income: Optional(0.0), category_code: Optional(99999), set_aside: Optional(true), set_aside_id: nil)
budgetRec in .sheet = BudgetedIncomeRecord(income_id: nil, account_code: nil, income_code: nil, budget_year: nil, budget_month: nil, description: nil, category: nil, income_budget: nil, income_received: nil, unexpected_income: nil, category_code: nil, set_aside: nil, set_aside_id: nil)
It seems to me that somewhere the data goes nil. I don't seem to follow where the data is going out of scope???
I have used this technique in other views where I have used a view for the row and let the whole row be selected .onGesture.
I haven't tried it using LazyVGrid and selecting specific output values..
Any help would be greatly appreciated..
Bob
I have a code that makes a http Request, gets an array with filenames from that, displays them each with an image and the filename below. Everything works fine.
Now I made each image a button that opens a detail page.
That works but at the top it should say the matching filename from the page before.
But I am not able to hand over the filename (name) from ContentView4 to the next page (ts).
The language is SwiftUi
Could you please help me?
Thanks
Nikias
Here is my code:
import SwiftUI
struct ContentView4: View {
#State var showingDetail = false
#State var username: String = "."
#State var password: String = "."
#State private var name = String("Nikias2")
#State private var t = String()
#State private var x = -1
#State var dateien = ["word.png"]
var body: some View {
ScrollView(.vertical) {
ZStack{
VStack {
ForEach(0 ..< dateien.count, id: \.self) {
Button(action: {
print("button pressed")
x = x + 1
t = dateien[x]
self.showingDetail.toggle()
}) {
Image("datei")
}
.scaledToFit()
.padding(0)
Text(self.dateien[$0])
Text(t)
.foregroundColor(.white)
}
}
}
.sheet(isPresented:
$showingDetail) {
ts(name: t)
}
.onAppear { //# This `onAppear` is added to `ZStack{...}`
doHttpRequest()
}
}
}
func doHttpRequest() {
let myUrl = URL(string: "http://192.168.1.180/int.php")! //# Trailing semicolon is not needed
var request = URLRequest(url: myUrl)
request.httpMethod = "POST"// Compose a query string
let postString = "Name=\($username)&Passwort=\($password)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
//# Use if-let when you want to use the unwrapped value
if let error = error {
print("error=\(error)")
return
}
//# Use guard-let when nil has no meaning and want to exit on nil
guard let response = response else {
print("Unexpected nil response")
return
}
// You can print out response object
print("response = \(response)")
//Let's convert response sent from a server side script to a NSDictionary object:
do {
//# Use guard-let when nil has no meaning and want to exit on nil
guard let data = data else {
print("Unexpected nil data")
return
}
//#1 `mutableContainer` has no meaning in Swift
//#2 Use Swift Dictionary type instead of `NSDictionary`
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
if let parseJSON = json {
// Now we can access value of First Name by its key
//# Use if-let when you want to use the unwrapped value
if let firstNameValue = parseJSON["Name"] as? String {
print("firstNameValue: \(firstNameValue)")
let dateien = firstNameValue.components(separatedBy: ",")
print(dateien)
self.dateien = dateien
}
}
} catch {
print(error)
}
}
task.resume()
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
ContentView4()
}
}
struct ts: View {
#State var hin = false
#State var um = false
#State var datname: String = ""
var name: String
var body: some View {
NavigationView {
VStack {
Text(name)
.font(.system(size: 60))
.foregroundColor(.black)
.padding(50)
Button(action: {
self.hin.toggle()
}) {
Text("+")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.yellow)
.cornerRadius(35.0)
}
.padding()
if hin {
HStack {
Text("Datei auswählen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.yellow)
.cornerRadius(20.0)
.animation(Animation.default)
Text("Datei hochladen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.yellow)
.cornerRadius(20.0)
.animation(Animation.default)
}
}
Text("Datei herunterladen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.blue)
.cornerRadius(35.0)
Button(action: {
self.um.toggle()
}) {
Text("Datei umbenennen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.green)
.cornerRadius(35.0)
}
.padding()
if um {
HStack {
TextField(name, text: $datname)
.font(.headline)
.frame(width: 150, height: 70)
.cornerRadius(20.0)
.animation(Animation.default)
Text("Datei umbenennen")
.font(.headline)
.frame(width: 150, height: 70)
.background(Color.green)
.cornerRadius(20.0)
.animation(Animation.default)
}
}
Text("Datei löschen")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.red)
.cornerRadius(35.0)
}
}
}
}
I believe your issue is a result of using #State variables to store all of the attributes. #State variables are not consistent and get refreshed in the background by SwiftUI depending on your views visibility.
The piece that you are missing is a view controller class stored in an #EnviornmentObject variable. This class gets Initiated in your main contentView and is used to keep track and alter of all your attributes.
Each ContentView should reference the single #EnviornmentObject and pull data from that class.
Another solution which may work would be to replace all your #State variables with #StateObject vars. #StateObject vars are basically #State vars but get initiated before the struct get loaded and the value is kept consistent regardless of the view state of the parent struct.
Here is a rough implementation of #EnvironmentObject within your project.
Basically use the #EnvironmentObject to pass values to child views
ContentView4.swift
struct ContentView4: View {
#EnvironmentObject cv4Controller: ContentView4Controller
var body: some View {
ScrollView(.vertical) {
ZStack{
VStack {
ForEach(0 ..< cv4Controller.dateien.count, id: \.self) {
Button(action: {
print("button pressed")
x = x + 1
t = cv4Controller.dateien[x]
self.showingDetail.toggle()
}) {
Image("datei")
}
.scaledToFit()
.padding(0)
Text(self.dateien[$0])
Text(cv4Controller.t)
.foregroundColor(.white)
}
}
}
.sheet(isPresented:
cv4Controller.$showingDetail) {
ts(name: cv4Controller.t)
}
.onAppear { //# This `onAppear` is added to `ZStack{...}`
cv4Controller.doHttpRequest()
}
}
}
ContentView4Controller.swift
class ContentView4Controller: ObservableObject {
#Published var showingDetail = false
#Published var username: String = "."
#Published var password: String = "."
#Published private var name = String("Nikias2")
#Published private var t = String()
#Published private var x = -1
#Published private var t = String()
#Published private var x = -1
#Published var dateien = ["word.png"]
func doHttpRequest() {
let myUrl = URL(string: "http://192.168.1.180/int.php")! //# Trailing semicolon is not needed
var request = URLRequest(url: myUrl)
request.httpMethod = "POST"// Compose a query string
let postString = "Name=\($username)&Passwort=\($password)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
//# Use if-let when you want to use the unwrapped value
if let error = error {
print("error=\(error)")
return
}
//# Use guard-let when nil has no meaning and want to exit on nil
guard let response = response else {
print("Unexpected nil response")
return
}
// You can print out response object
print("response = \(response)")
//Let's convert response sent from a server side script to a NSDictionary object:
do {
//# Use guard-let when nil has no meaning and want to exit on nil
guard let data = data else {
print("Unexpected nil data")
return
}
//#1 `mutableContainer` has no meaning in Swift
//#2 Use Swift Dictionary type instead of `NSDictionary`
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
if let parseJSON = json {
// Now we can access value of First Name by its key
//# Use if-let when you want to use the unwrapped value
if let firstNameValue = parseJSON["Name"] as? String {
print("firstNameValue: \(firstNameValue)")
let dateien = firstNameValue.components(separatedBy: ",")
print(dateien)
self.dateien = dateien
}
}
} catch {
print(error)
}
}
task.resume()
}
}
Example of main ContentView.swift
struct ContentView: View {
var cv4Controller: ContentView4Controller = ContentView4Controller()
var body: some view {
// your main page output
GeometryReader { geo in
// just a guess for what you have in your main contentView
switch(page) {
case .main:
ContentView2()
default:
ContentView4()
break
}
}.environmentObject(cv4Controller) // this will make cv4Controller available to all child view structs
}
}
Add #Binding wrapper to the "name" variable in your ts view. And pass the t variable as a binding by adding a "$". This will keep your ts name variable updated to whatever is value it has in the parent view.
Also why do you use a NavigationView in your ts View?
struct ContentView4: View {
...
#State private var t = String()
...
var body: some View {
...
ZStack{
...
}
.sheet(isPresented: $showingDetail) {
ts(name: $t)
}
...
}
func doHttpRequest() {
...
}
}
struct ts: View {
...
#Binding var name: String
var body: some View {
...
}
}
My starting code works, but It's just displaying the Filenames in a row and if I tap a random image, the name won't fit, only if I'm going down in the row and tap them. The problem is, that I don't know how to set the variable to the id, not to pass them to the next view. Has anyone got and idea how I can pass the right filename into a variable in the for loop and read it in the next view?
I'm having trouble with usage of a count of the number of entries in a view. I especially need to know when there are no entries in the view. I have placed debug code in the view below and the view count currants.curItem.countis updating as expected. The count status in checkForUpdates() doesn't follow the view above.
If I recall correctly I should be using #EnvironmentObject or #ObservedObject only in a view. I really need some kind of global variable that I can pass to the method checkForUpdates. It is crashing when count in checkForUpdates() is nonzero when in the view it is actually zero. It also crashes in checkForUpdates() with the error Fatal error: No ObservableObject of type Currencies found. A View.environmentObject(_:) for Currencies may be missing as an ancestor of this view.
struct manCurView: View {
#EnvironmentObject var currants: Currants
var body: some View {
List {
ForEach(currants.curItem, id: \.id) { item in
HStack {
Text(item.curCode)
.frame(width: 100, alignment: .center)
Text(item.cunName)
}
.font(.subheadline)
}
.onDelete(perform: removeItems)
}
.navigationBarTitle(Text("Manage Working Blocks"), displayMode: .inline)
HStack {
NavigationLink(destination: addCurView()) {Text("Add Working Blocks").fontWeight(.bold)}
.font(.title2)
.disabled(currants.curItem.count > 7)
Here is how the data is stored for the view above
struct CurItem: Codable, Identifiable {
var id = UUID()
var cunName: String
var curName: String
var curCode: String
var curSymbol: String
var curRate: Double
}
class Currants: ObservableObject {
#Published var curItem: [CurItem]
}
And here is the class and method where I would like to use count from the view manCurView
class BlockStatus: ObservableObject {
#EnvironmentObject var globalCur : Currants
#ObservedObject var netStatus : TestNetStatus = TestNetStatus()
func checkForUpdates() -> (Bool) {
if netStatus.connected == true {
if globalCur.curItem.count > 0 {
Without a minimal reproducible example it is very difficult to give you exact code but you can try something like the code below in your manCurView
#StateObject var blockStatus: BlockStatus = BlockStatus()
.onChange(of: currants.curItem.count, perform: { value in
print("send value from here")
blockStatus.arrayCount = value
})
And adding the code below to BlockStatus
#Published var arrayCount: Int = 0{
didSet{
//Call your method here
}
}
Look at the code below.
import SwiftUI
import Combine
struct CurItem: Codable, Identifiable {
var id = UUID()
}
class Currants: ObservableObject {
#Published var curItem: [CurItem] = [CurItem(), CurItem(), CurItem(), CurItem()]
}
class TestNetStatus: ObservableObject {
static let sharedInstance = TestNetStatus()
#Published var connected: Bool = false
init() {
//Simulate changes in connection
Timer.scheduledTimer(withTimeInterval: 10, repeats: true){ timer in
self.connected.toggle()
}
}
}
class BlockStatus: ObservableObject {
#Published var arrayCount: Int = 0{
didSet{
checkForUpdates()
}
}
#Published var checkedForUpdates: Bool = false
var netStatus : TestNetStatus = TestNetStatus.sharedInstance
//private var cancellable: AnyCancellable?
init() {
//Maybe? if you want to check upon init.
//checkForUpdates()
//Something like the code below is also possible but with 2 observed objects the other variable could be outdated
// cancellable = netStatus.objectWillChange.sink { [weak self] in
// self?.checkForUpdates()
// }
}
func checkForUpdates() {
if netStatus.connected == true {
if arrayCount > 0 {
checkedForUpdates = true
}else{
checkedForUpdates = false
}
}else{
checkedForUpdates = false
}
}
}
struct ManCurView: View {
#StateObject var currants: Currants = Currants()
#StateObject var blockStatus: BlockStatus = BlockStatus()
#StateObject var testNetStatus: TestNetStatus = TestNetStatus.sharedInstance
var body: some View {
List {
Text("checkedForUpdates = " + blockStatus.checkedForUpdates.description).foregroundColor(blockStatus.checkedForUpdates ? Color.green : Color.red)
Text("connected = " + blockStatus.netStatus.connected.description).foregroundColor(blockStatus.netStatus.connected ? Color.green : Color.red)
ForEach(currants.curItem, id: \.id) { item in
HStack {
Text(item.id.uuidString)
.frame(width: 100, alignment: .center)
Text(item.id.uuidString)
}
.font(.subheadline)
}
//Replaced with toolbar button for sample
//.onDelete(perform: removeItems)
//When the array count changes
.onChange(of: currants.curItem.count, perform: { value in
blockStatus.arrayCount = value
})
//Check when the networkStatus changes
.onChange(of: testNetStatus.connected, perform: { value in
//Check arrayCount
if blockStatus.arrayCount != currants.curItem.count{
blockStatus.arrayCount = currants.curItem.count
}else{
blockStatus.checkForUpdates()
}
})
}
.navigationBarTitle(Text("Manage Working Blocks"), displayMode: .inline)
//Replaced addCurView call with toolbar button for sample
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing, content: {
Button("add-currant", action: {
currants.curItem.append(CurItem())
})
})
ToolbarItem(placement: .navigationBarLeading, content: {
Button("delete-currant", action: {
if currants.curItem.count > 0{
currants.curItem.removeFirst()
}
})
})
})
}
}
Here is ContentView: Notice in the menu that because this is a view I can use count directly to disable entry input. Down in getData() notice that I'm calling blockStatus.checkForUpdates() to determine if is OK to call the API. A fault will occur if currants.curItem.count = 0
I just realized that technically getData() is part of the ContentView so I can change the call below to if blockStatus.checkForUpdates() == true && currants.curItem.count != 0 {
I'm going to spend some time studying your suggestions above to see if I could use this in the future.
So thanks for all the help by looking into this. I wasn't aware of the suggestions on code displayed on Stackoverflow. I'll be sure to follow those guidelines in the future. Galen
import SwiftUI
import CoreData
import Combine
struct ContentView: View {
#EnvironmentObject var userData: UserData
#EnvironmentObject var currants: Currants
#EnvironmentObject var blockStatus: BlockStatus
var body: some View {
NavigationView {
VStack (alignment: .center) {
Text("Title")
.font(.title)
.fontWeight(.bold)
Spacer()
Group {
NavigationLink(destination: entryView()) {Text("Entry")}
.disabled(currants.curItem.count == 0)
Spacer()
NavigationLink(destination: totalView()) {Text("View Totals")}
Spacer()
NavigationLink(destination: listView()) {Text("View Entries")}
Spacer()
NavigationLink(destination: xchView()) {Text("View Dates")}
}
Rectangle()
.frame(height: 130)
.foregroundColor(Color.white)
}
.font(.title2)
.navigationBarItems(leading: NavigationLink (destination: settingsView()) {
Image(systemName: "gear")
.foregroundColor(.gray)
.font(.system(.title3))
}, trailing: NavigationLink( destination: aboutView()) {
Text("About")
})
.onAppear(perform: getData)
}
}
func getData() {
// check criteria for updating data once daily
if blockStatus.checkForUpdates() == true {
print(" doing update")
---- API HERE -----
}.resume()
}
}
}
[Edit(1) to reflect posting of streamlined app to illustrate the issue : ].
[Edit (2) : completely removed EnvironmentObject and app now works ! Not understanding WHY body is refreshed as NO #State vars are being modified...Code at end of text]
I am writing an app that, at some point, displays some text, related to the contents of 2 Arrays, depending on a set of rules. These rules can be set in a Settings view, as User's preference.
So, when a user changes the rules he wants applied in Settings, that text needs to be re-assessed.
But of course, things aren't that easy.
I present my settings view as modal on my main ContentView, and when I dismiss that modal, the body of the ContentView is not redrawn...
I created an EnvironmentObject with #Published vars in order to keep track of all the user preferences (that are also written to UserDefaults), and shared that #EnvironmentObject with both my ContentView and SettingsView, in the hope that, being an observedObject, its changes would trigger a refresh of my ContentView.
Not so...
Any ideas to help me go forward on this ? Any pointers would be greatly appreciated (again!).
Posted app on GitHub has following architecture :
An appState EnvironmentObject,
A ContentView that displays a set of texts, depending on some user preferences set in
A settingsView
UserDefaults are initialized in AppDelegate.
Thanks for any help on this...
Content view :
import SwiftUI
struct ContentView: View {
#State var modalIsPresented = false // The "settingsView" modally presented as a sheet
#State private var modalViewCaller = 0 // This triggers the appropriate modal (only one in this example)
var body: some View {
NavigationView {
VStack {
Spacer()
VStack {
Text(generateStrings().text1)
.foregroundColor(Color(UIColor.systemGreen))
Text(generateStrings().text2)
} // end of VStack
.frame(maxWidth: .infinity, alignment: .center)
.lineLimit(nil) // allows unlimited lines
.padding(.all)
Spacer()
} // END of main VStack
.onAppear() {
self.modalViewCaller = 0
}
.navigationBarTitle("Test app", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
self.modalViewCaller = 6 // SettingsView
self.modalIsPresented = true
}
) {
Image(systemName: "gear")
.imageScale(.large)
}
))
} // END of NavigationView
.sheet(isPresented: $modalIsPresented, content: sheetContent)
.navigationViewStyle(StackNavigationViewStyle()) // This avoids dual column on iPad
} // END of var body: some View
// MARK: #ViewBuilder func sheetContent() :
#ViewBuilder func sheetContent() -> some View {
if modalViewCaller == 6 {
SettingsView()
}
} // END of func sheetContent
// MARK: generateStrings() : -
func generateStrings() -> (text1: String, text2: String, recapText: String, isHappy: Bool) { // minimumNumberOfEventsCheck
var myBool = false
var aString = "" // The text 1 string
var bString = "" // The text 2 string
var cString = "" // The recap string
if UserDefaults.standard.bool(forKey: kmultiRules) { // The user chose the dual rules option
let ruleSet = UserDefaults.standard.integer(forKey: kruleSelection) + 1
aString = "User chose 2 rules option"
bString = "User chose rule set # \(ruleSet)"
myBool = true
print("isDualRules true loop : generateStrings was called at \(Date().debugDescription)")
cString = "Dual rules option, user chose rule set nb \(ruleSet)"
}
else // The user chose the single rule option
{
aString = "User chose single rule option"
bString = "User had no choice : there is only one set of rules !"
myBool = false
print("isDualRules false loop : generateStrings was called at \(Date().debugDescription)")
cString = "Single rule option, user chose nothing."
}
return (aString, bString, cString, myBool)
} // End of func generatestrings() -> String
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return ContentView()
}
}
SettingsView :
import SwiftUI
import UIKit
struct SettingsView: View {
#Environment(\.presentationMode) var presentationMode // in order to dismiss the Sheet
#State public var multiRules = UserDefaults.standard.bool(forKey: kmultiRules)
#State private var ruleSelection = UserDefaults.standard.integer(forKey: kruleSelection) // 0 is rule 1, 1 is rule 2
var body: some View {
NavigationView {
List {
Toggle(isOn: $multiRules)
{
Text("more than one rule ?")
}
.padding(.horizontal)
if multiRules {
Picker("", selection: $ruleSelection){
Text("rules 1").tag(0)
Text("rules 2").tag(1)
}.pickerStyle(SegmentedPickerStyle())
.padding(.horizontal)
}
} // End of List
.navigationBarItems(
leading:
Button("Done") {
self.saveDefaults() // We try to save once more if needed
self.presentationMode.wrappedValue.dismiss() // This dismisses the view
}
)
.navigationBarTitle("Settings", displayMode: .inline)
} // END of Navigation view
} // END of some View
func saveDefaults() {
UserDefaults.standard.set(multiRules, forKey: kmultiRules)
UserDefaults.standard.set(ruleSelection, forKey: kruleSelection)
}
}
// MARK: Preview struct
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
return SettingsView()
}
}
Constants.swift file :
import Foundation
import SwiftUI
let kmultiRules = "two rules"
let kruleSelection = "rules selection"
let kappStateChanged = "appStateChanged"
AppDelegate :
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UserDefaults.standard.register(defaults: [ // We initialize the UserDefaults
"two rules": false,
"rules selection": 0, // 0 is ruel 1, 1 is rule 2
"appStateChanged": false
])
return true
}
If you have a shared #EnvironmentObject with #Published properties in two views, if you change such a property from one view, the other one will be re-execute the body property and the view will be updated.
It really helps to create simple standalone examples - not only for asking here, also for gaining a deeper understanding / getting an idea why it doesn't work in the complex case.
For example:
import SwiftUI
class TextSettings: ObservableObject {
#Published var count: Int = 1
}
struct TextSettingsView: View {
#EnvironmentObject var settings: TextSettings
var body: some View {
Form {
Picker(selection: $settings.count, label:
Text("Text Repeat Count"))
{
ForEach(Array(1...5), id: \.self) { value in
Text(String(value)).tag(value)
}
}
}
}
}
struct TextWithSettingExampleView: View {
#EnvironmentObject var settings: TextSettings
var body: some View {
Text(String(repeating: "Hello ", count: Int(settings.count)))
.navigationBarItems(trailing: NavigationLink("Settings", destination: TextSettingsView()))
}
}
struct TextWithSettingExampleView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
TextWithSettingExampleView()
}
.environmentObject(TextSettings())
}
}
Not sure I fully understand the question, but I had what I believe might be a similar problem where I never got my contentview to reflect the updates in my observed object when the changes were triggered from a modal. I solved/hacked this by triggering an action in my observed object when dismissing the modal like this:
struct ContentView: View {
//
#State var isPresentingModal = false
var body: some View {
//
.sheet(isPresented: self.$isPresentingModal) {
PresentedModalView()
.onDisappear {
//Do something here
}
}
}
}