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]
}
Related
In the following example code, a SwiftUI form holds an Observable object that holds a trivial pipeline that passes a string through to a #Published value. That object is being fed by the top line of the SwiftUI form, and the output is being displayed on the second line.
The value in the text field in the first row gets propagated to the output line in the second row, whenever we hit the "Send" button, unless we hit the "End" button, which cancels the subscription, as we'd expect.
import SwiftUI
import Combine
class ResetablePipeline: ObservableObject {
#Published var output = ""
var input = PassthroughSubject<String, Never>()
init(output: String = "") {
self.output = output
self.input
.assign(to: &$output)
}
func reset()
{
// What has to go here to revive a completed pipeline?
self.input
.assign(to: &$output)
}
}
struct ResetTest: View {
#StateObject var pipeline = ResetablePipeline()
#State private var str = "Hello"
var body: some View {
Form {
HStack {
TextField(text: $str, label: { Text("String to Send")})
Button {
pipeline.input.send(str)
} label: {
Text("Send")
}.buttonStyle(.bordered)
Button {
pipeline.input.send(completion: .finished)
} label: {
Text("End")
}.buttonStyle(.bordered)
}
Text("Output: \(pipeline.output)")
Button {
pipeline.reset()
} label: {
Text("Reset")
}
}
}
}
struct ResetTest_Previews: PreviewProvider {
static var previews: some View {
ResetTest()
}
}
My understanding is that hitting "End" and completing/cancelling the subscription will delete all the Combine nodes that were set up in the ResetablePipeline.init function (currently only the assign operator).
But if we wanted to reset that connection, how would we do that (without creating a new ResetablePipeline object). What would you have to do in reset() to reconnect the plumbing in the ResetablePipeline object, so that the Send button would work again? Why does the existing code not work?
It is part of the fundamental nature of a Publisher that once the Publisher has finished, or has emitted an error, that the publisher will never emit another value.
This is described in Reactive X in the Observable Contract
The fundamental reason for this is that when the pipeline finishes, the stages in the pipeline are free to release any resources they may have obtained. For example, if a collect operator has set aside memory for its connected items, it can release that memory once the pipeline finishes.
In short, there is no way to do what you want to do. You cannot restart a pipeline that has finished, though you can construct a new one.
Well I'll be. If I simply add input = PassthroughSubject<String, Never>() to the start of reset() (ie replace the original cancelled head-publisher with a fresh one), it seems to do the trick.
Now, I'm not entirely sure if this code is not leaking something since I don't know exactly what assign(to:) does with the old subscription, but assuming that it's sensible, this might be OK.
Can anyone see anything wrong with this approach?
Why running this code shows "Fatal error: Index out of range"?
import SwiftUI
struct MyData {
var numbers = [Int](repeating: 0, count: 5)
}
#main
struct TrySwiftApp: App {
#State var myData = MyData()
var body: some Scene {
WindowGroup {
ChildView(myData: myData)
.frame(width: 100, height: 100)
.onAppear {
myData.numbers.removeFirst() // change myData
}
}
}
}
struct ChildView: View {
let myData: MyData // a constant
var body: some View {
ForEach(myData.numbers.indices) {
Text("\(myData.numbers[$0])") // Thread 1: Fatal error: Index out of range
}
}
}
After checking other questions,
I know I can fix it by following ways
// fix 1: add id
ForEach(myData.numbers.indices, id: \.self) {
//...
}
or
// Edited:
//
// This is not a fix, see George's reply
//
// fix 2: make ChildView conforms to Equatable
struct ChildView: View, Equatable {
static func == (lhs: ChildView, rhs: ChildView) -> Bool {
rhs.myData.numbers == rhs.myData.numbers
}
...
My Questions:
How a constant value (defined by let) got out of sync?
What ForEach really did?
Let me give you a simple example to show you what happened:
struct ContentView: View {
#State private var lowerBound: Int = 0
var body: some View {
ForEach(lowerBound..<11) { index in
Text(String(describing: index))
}
Button("update") { lowerBound = 5 }.padding()
}
}
if you look at the upper code you would see that I am initializing a ForEach JUST with a Range like this: lowerBound..<11 which it means this 0..<11, when you do this you are telling SwiftUI, hey this is my range and it will not change! It is a constant Range! and SwiftUI says ok! if you are not going update upper or lower bound you can use ForEach without showing or given id! But if you see my code again! I am updating lowerBound of ForEach and with this action I am breaking my agreement about constant Range! So SwiftUI comes and tell us if you are going update my ForEach range in count or any thing then you have to use an id then you can update the given range! And the reason is because if we have 2 same item with same value, SwiftUI would have issue to know which one you say! with using an id we are solving the identification issue for SwiftUI! About id you can use it like this: id:\.self or like this id:\.customID if your struct conform to Hash-able protocol, or in last case you can stop using id if you confrom your struct to identifiable protocol! then ForEach would magically sink itself with that.
Now see the edited code, it will build and run because we solved the issue of identification:
struct ContentView: View {
#State private var lowerBound: Int = 0
var body: some View {
ForEach(lowerBound..<11, id:\.self) { index in
Text(String(describing: index))
}
Button("update") { lowerBound = 5 }.padding()
}
}
Things go wrong when you do myData.numbers.removeFirst(), because now myData.numbers.indices has changed and so the range in the ForEach showing Text causes problems.
You should see the following warning (at least I do in Xcode 13b5) hinting this could cause issues:
Non-constant range: not an integer range
The reason it is not constant is because MyData's numbers property is a var, not let, meaning it can change / not constant - and you do change this. However the warning only shows because you aren't directly using a range literal in the ForEach initializer, so it assumes it's not constant because it doesn't know.
As you say, you have some fixes. Solution 1 where you provide id: \.self works because now it uses a different initializer. Definition for the initializer you are using:
#available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ForEach where Data == Range<Int>, ID == Int, Content : View {
/// Creates an instance that computes views on demand over a given constant
/// range.
///
/// The instance only reads the initial value of the provided `data` and
/// doesn't need to identify views across updates. To compute views on
/// demand over a dynamic range, use ``ForEach/init(_:id:content:)``.
///
/// - Parameters:
/// - data: A constant range.
/// - content: The view builder that creates views dynamically.
public init(_ data: Range<Int>, #ViewBuilder content: #escaping (Int) -> Content)
}
Stating:
The instance only reads the initial value of the provided data and doesn't need to identify views across updates. To compute views on demand over a dynamic range, use ForEach/init(_:id:content:).
So that's why your solution 1 worked. You switched to the initializer which didn't assume the data was constant and would never change.
Your solution 2 isn't really a "solution". It just doesn't update the view at all, because myData.numbers changes so early that it is always equal, so the view never updates. You can see the view still has 5 lines of Text, rather than 4.
If you still have issues with accessing the elements in this ForEach and get out-of-bounds errors, this answer may help.
I think I need to change a boolean in a UIRepresentableView before removing it from the ContentView but can't find a way, since I can't mutate the struct properties.
Why do I need that? Because I need to prevent updateUIView() from being called one last time before the view being released.
So probably this is an XY problem... anyway, I'm lost.
I have a model like this:
class PlayerModel: NSObject, ObservableObject {
#Published var lottie: [LottieView]
Where LottieView is this:
struct LottieView: UIViewRepresentable {
var name: String
var loopMode: LottieLoopMode = .playOnce
func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
let view = UIView()
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieView>) {
uiView.subviews.forEach({ sub in
sub.removeFromSuperview()
})
let animationView = AnimationView()
animationView.translatesAutoresizingMaskIntoConstraints = false
uiView.addSubview(animationView)
NSLayoutConstraint.activate([
animationView.widthAnchor.constraint(equalTo: uiView.widthAnchor),
animationView.heightAnchor.constraint(equalTo: uiView.heightAnchor)
])
animationView.animation = Animation.named(name, animationCache: LRUAnimationCache.sharedCache)
animationView.contentMode = .scaleAspectFit
animationView.loopMode = loopMode
animationView.play()
}
}
In my ContentView I only display a LottieView if there's a recent one in the published array:
struct ContentView: View {
#ObservedObject var model: PlayerModel
var body: some View {
NavigationView {
VStack {
// ...
if let lottie = model.lottie.last {
lottie
}
// ...
}
}
}
}
Now this works ok, but when in the model I remove the last LottieView from the published array, the updateUIView() func is called one last time by SwiftUI, resulting in the animation being recreated again just before the LottieView is removed from the view, resulting in a visible flicker.
So I thought I'd add a boolean that I could update just before removing the last LottieView from the published array, and do an if/else in the updateUIView(), but... is this even possible? I can't find a way.
Ideally I would just have the animation view created in makeUIView() and nothing done in updateUIView() but this is not possible, I need to remove the subviews first, otherwise the previous shown animation is not replaced by the new one.
While I would still love to have someone give me a proper solution to my issue, or tell me an alternative way of doing things, I found a workaround.
In the model, I was previously emptying the published array by doing
lottie = []
But if instead we do this:
lottie.removeAll()
We see no flicker anymore (however updateView() is still called!!).
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 !!
}
}
}
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 }))
}
...