Is it possible to create a global #State variable in SwiftUI that can be accessed across multiple Swift UI files?
I've looked into #EnvironmentObject variables but can't seem to make them do what I want them to do.
As of Beta 3 you cannot create a top-level global #State variable. The compiler will segfault. You can place one in a struct and create an instance of the struct in order to build. However, if you actually instantiate that you'll get a runtime error like: Accessing State<Bool> outside View.body.
Probably what you're looking for is an easy way to create a binding to properties on a BindableObject. There's a good example of that in this gist.
It is possible to create a Binding to a global variable, but unfortunately this still won't do what you want. The value will update, but your views will not refresh (code example below).
Example of creating a Binding programmatically:
var globalBool: Bool = false {
didSet {
// This will get called
NSLog("Did Set" + globalBool.description)
}
}
struct GlobalUser : View {
#Binding var bool: Bool
var body: some View {
VStack {
Text("State: \(self.bool.description)") // This will never update
Button("Toggle") { self.bool.toggle() }
}
}
}
...
static var previews: some View {
GlobalUser(bool: Binding<Bool>(getValue: { globalBool }, setValue: { globalBool = $0 }))
}
...
Related
I' trying to create a set of Toggles, that need to be stored in one core data field of type "Transformable". I started with this example:
https://developer.apple.com/forums/thread/118595
in combination with other ideas from stack.
I'm trying to get this way:
Create a Set of structs like this
struct AllScopes: Identifiable, Hashable {
var id: UUID
var name: String
var notify: Bool
}
[...]
// all the stuff with View and body with
#State var scopes = Set<AllScopes>()
[...]
// and here I run through my FetchRequest to fill the Set
.onAppear {
for scope in allScopes {
scopes.insert(
AllScopes(
id: scope.id!,
name: scope.name!,
notify: false
)
)
}
}
In the end I've got a nice Set with all my scopes.
I call a new View with YearlyReportPage6(scopes: $scopes)
And now my problem - the next view:
struct YearlyReportPage6: View {
#Binding var scopes: Set<AllScopes>
init(scopes: Binding<Set<AllScopes>>) {
_scopes = scopes
}
var body: some View {
VStack {
ForEach(scopes.indices) { index in
Toggle(isOn: self.$scopes[index].notify) {
Text(self.scopes[index].name)
}
}
}
}
}
But all in ForEach creates errors. Either Binding in isOn: is wrong, or ForEach can't work with the set, or the Text is not a String, or, or, or...
In the end there should be a list of Toggles (checkboxes) and the selection should be stored in database.
Changing the Set into a simple Array like #State var scopes = [AllScopes]() will work for the Toggles, but how can I store this into a Transformable?
ForEach(Array(scopes).indices) { index in
Toggle(isOn: self.$scopes[index].notify) {
Text(self.scopes[index].name)
}
}
To summarize:
either how can I create the list of Toggles with the Set of AllScopes
or how can I store the Array / Dictionary into the Transformable field?
I hope, you can understand my clumsy English. :-)
I am trying to pass an array to another view. I want to pass the array simulation to operating conditions view.
#State var simulation = [Any]()
I know you can not see all the code but below is where if a button is pressed to show the operating conditions view and pass in an array after the array is loaded with data I have checked to make sure Simulation array does have values in it before passing and it does.
.sheet(isPresented: $showingOperatingConditions) {
OperatingConditionsView(threats: simulation)
}
Here is the operating conditions view where threats is declared. For some reason, the array is empty every time I load the view. Can anybody help?
struct OperatingConditionsView: View {
#State public var threats = [Any]()
}
As the comments said, you want to change OperatingConditionsView's threats into a #Binding. You'll then need to pass in $simulation with a $ (gets its Binding).
.sheet(isPresented: $showingOperatingConditions) {
OperatingConditionsView(threats: $simulation) /// need a $ here
}
struct OperatingConditionsView: View {
#Binding public var threats: [Any] /// don't initialize with default value
}
Alternatively, if you don't need changes in threats to auto-update simulation, you can go with a plain, non-binding property as #Joakim Danielson's comment said.
.sheet(isPresented: $showingOperatingConditions) {
OperatingConditionsView(threats: simulation)
}
struct OperatingConditionsView: View {
public var threats: [Any]
}
When creating a class conforming to ReferenceFileDocument, how do you indicate the document needs saving. i.e. the equivalent of the NSDocument's updateChangeCount method?
I've met the same problem that the SwiftUI ReferenceFileDocument cannot trigger the update. Recently, I've received feedback via the bug report and been suggested to register an undo.
Turns out the update of ReferenceFileDocument can be triggered, just like UIDocument, by registering an undo action. The difference is that the DocumentGroup explicitly implicitly setup the UndoManager via the environment.
For example,
#main
struct RefDocApp: App {
var body: some Scene {
DocumentGroup(newDocument: {
RefDocDocument()
}) { file in
ContentView(document: file.document)
}
}
}
struct ContentView: View {
#Environment(\.undoManager) var undoManager
#ObservedObject var document: RefDocDocument
var body: some View {
TextEditor(text: Binding(get: {
document.text
}, set: {
document.text = $0
undoManager?.registerUndo(withTarget: document, handler: {
print($0, "undo")
})
}))
}
}
I assume at this stage, the FileDocument is actually, on iOS side, a wrapper on top of the UIDocument, the DocumentGroup scene explicitly implicitly assign the undoManager to the environment. Therefore, the update mechanism is the same.
The ReferenceFileDocument is ObservableObject, so you can add any trackable or published property for that purpose. Here is a demo of possible approach.
import UniformTypeIdentifiers
class MyTextDocument: ReferenceFileDocument {
static var readableContentTypes: [UTType] { [UTType.plainText] }
func snapshot(contentType: UTType) throws -> String {
defer {
self.modified = false
}
return self.storage
}
#Published var modified = false
#Published var storage: String = "" {
didSet {
self.modified = true
}
}
}
ReferenceFileDocument exists for fine grained controll over the document. In comparison, a FileDocument has to obey value semantics which makes it very easy for SwiftUI to implement the undo / redo functionality as it only needs to make a copy before each mutation of the document.
As per the documentation of the related DocumentGroup initializers, the undo functionality is not provided automatically. The DocumentGroup will inject an instance of an UndoManger into the environment which we can make use of.
However an undo manager is not the only way to update the state of the document. Per this documentation AppKit and UIKit both have the updateChangeCount method on their native implementation of the UI/NSDocument object. We can reach this method by grabbing the shared document controller on macOS from within the view and finding our document. Unfortunately I don't have a simple solution for the iOS side. There is a private SwiftUI.DocumentHostingController type which holds a reference to our document, but that would require mirroring into the private type to obtain the reference to the native document, which isn't safe.
Here is a full example:
import SwiftUI
import UniformTypeIdentifiers
// DOCUMENT EXAMPLE
extension UTType {
static var exampleText: UTType {
UTType(importedAs: "com.example.plain-text")
}
}
final class MyDocument: ReferenceFileDocument {
// We add `Published` for automatic SwiftUI updates as
// `ReferenceFileDocument` refines `ObservableObject`.
#Published
var number: Int
static var readableContentTypes: [UTType] { [.exampleText] }
init(number: Int = 42) {
self.number = number
}
init(configuration: ReadConfiguration) throws {
guard
let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8),
let number = Int(string)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.number = number
}
func snapshot(contentType: UTType) throws -> String {
"\(number)"
}
func fileWrapper(
snapshot: String,
configuration: WriteConfiguration
) throws -> FileWrapper {
// For the sake of the example this force unwrapping is considered as safe.
let data = snapshot.data(using: .utf8)!
return FileWrapper(regularFileWithContents: data)
}
}
// APP EXAMPLE FOR MACOS
#main
struct MyApp: App {
var body: some Scene {
DocumentGroup.init(
newDocument: {
MyDocument()
},
editor: { file in
ContentView(document: file.document)
.frame(width: 400, height: 400)
}
)
}
}
struct ContentView: View {
#Environment(\.undoManager)
var _undoManager: UndoManager?
#ObservedObject
var document: MyDocument
var body: some View {
VStack {
Text(String("\(document.number)"))
Button("randomize") {
if let undoManager = _undoManager {
let currentNumber = document.number
undoManager.registerUndo(withTarget: document) { document in
document.number = currentNumber
}
}
document.number = Int.random(in: 0 ... 100)
}
Button("randomize without undo") {
document.number = Int.random(in: 0 ... 100)
// Let the system know that we edited the document, which will
// eventually trigger the auto saving process.
//
// There is no simple way to mimic this on `iOS` or `iPadOS`.
let controller = NSDocumentController.shared
if let document = controller.currentDocument {
// On `iOS / iPadOS` change the argument to `.done`.
document.updateChangeCount(.changeDone)
}
}
}
}
}
Unfortunatelly SwiftUI (v2 at this moment) does not provide a native way to mimic the same functionality, but this workaround is still doable and fairly consice.
Here is a gist where I extended the example with a custom DocumentReader view and a DocumentProxy which can be extended for common document related operations for more convenience: https://gist.github.com/DevAndArtist/eb7e8aa5e7134610c20b1a7aca358604
I've created a trivial project to try to understand this better. Code below.
I have a source of data (DataSource) which contains a #Published array of MyObject items. MyObject contains a single string. Pushing a button on the UI causes one of the MyObject instances to update immediately, plus sets off a timer to update a second one a few seconds later.
If MyObject is a struct, everything works as I imagine it should. But if MyObject is a class, then the refresh doesn't fire.
My expectation is that changing a struct's value causes an altered instance to be placed in the array, setting off the chain of updates. However, if MyObject is a class then changing the string within a reference type leaves the same instance in the array. Array doesn't realise there has been a change so doesn't mention this to my DataSource. No UI update happens.
So the question is – what needs to be done to cause the UI to update when the MyObject class's property changes? I've attempted to make MyObject an ObservableObject and throw in some didchange.send() instructions but all without success (I believe these are redundant now in any case).
Could anyone tell me if this is possible, and how the code below should be altered to enable this? And if anyone is tempted to ask why I don't just use a struct, the reason is because in my actual project I have already tried doing this. However I am using collections of data types which modify themselves in closures (parallel processing of each item in the collection) and other hoops to jump through. I tried re-writing them as structs but ran in to so many challenges.
import Foundation
import SwiftUI
struct ContentView: View
{
#ObservedObject var source = DataSource()
var body: some View
{
VStack
{
ForEach(0..<5)
{i in
HelloView(displayedString: self.source.results[i].label)
}
Button(action: {self.source.change()})
{
Text("Change me")
}
}
}
}
struct HelloView: View
{
var displayedString: String
var body: some View
{
Text("\(displayedString)")
}
}
class MyObject // Works if declared as a Struct
{
init(label: String)
{
self.label = label
}
var label: String
}
class DataSource: ObservableObject
{
#Published var results = [MyObject](repeating: MyObject(label: "test"), count: 5)
func change()
{
print("I've changed")
results[3].label = "sooner"
_ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: {_ in self.results[1].label = "Or later"})
}
}
struct ContentView_Previews: PreviewProvider
{
static var previews: some View
{
ContentView()
}
}
When MyObject is a class type the results contains references, so when you change property of any instance inside results the reference of that instance is not changed, so results is not changed, so nothing published and UI is not updated.
In such case the solution is to force publish explicitly when you perform any change of internal model
class DataSource: ObservableObject
{
#Published var results = [MyObject](repeating: MyObject(label: "test"), count: 5)
func change()
{
print("I've changed")
results[3].label = "sooner"
self.objectWillChange.send() // << here !!
_ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) {[weak self] _ in
self?.results[1].label = "Or later"
self?.objectWillChange.send() // << here !!
}
}
}
I am building a custom View struct with a large and complicated model that I want to update from within the var body : some View { ... } property (ex. tapping on a button representing a table column in the view should change the sort order of the rows in the table). I am not allowed to modify this model from within body because self is immutable however:
struct MyTable : View {
struct Configuration {
// a LOT of data here, ie header, rows, footer, style, etc
mutating func update() { ... } // this method must be called before view render
}
var configuration : Configuration
var body : some View {
// build the view here based on configuration
self.configuration.columns[3].sortAscending = false // error, `self` is immutable
self.configuration.update() // error, `self` is immutable
}
}
I really don't want to create #State variables for all of the configuration data because 1) there's a lot of configuration data, 2) it would be difficult to model the model in such a way.
I tried instead making configuration a #State var, but I am unable to set the configuration object at init() time even though the code compiles and runs! (BTW, the configuration var now needs to be initialized before init, otherwise I get an error on the line self.configuration = c that self is used before all members are initialized -- this is likely a complication with using #State which is a property wrapper.)
struct MyTable : View {
struct Configuration {
...
}
#State var configuration : Configuration = Configuration() // must initialize the #State var
init(configuration c: Configuration) {
self.configuration = c // does not assign `c` to `configuration` !!!
self.$configuration.wrappedValue = c // this also does not assign !!!
self.configuration.update()
}
var body : some View {
// build the view here based on configuration
self.configuration.columns[3].sortAscending = false // ok, no error now about mutating a #State var
self.configuration.update() // ok, no error
}
}
I came up with a work around where I don't need to call update() in MyTable.init() by creating a custom init() in Configuration that will call update(). This way the init() in MyTable is unnecessary and this approach resolves all previously encountered problems:
struct MyTable : View {
struct Configuration {
...
init(...) {
...
self.update()
}
mutating func update() { ... } // this method must be called before view render
}
#State var configuration : Configuration // note that this property can't be marked private because we want to inject `configuration` at init
var body : some View {
// build the view here based on configuration
self.configuration.columns[3].sortAscending = false // ok, no error now about mutating a #State var
self.configuration.update() // ok, no error
...
}
}
Then in my calling code:
return MyTable.init(configuration: MyTable.Configuration.init(...))