Init State var coming from another view , swiftui [duplicate] - swiftui

This question already has answers here:
SwiftUI #State var initialization issue
(8 answers)
Closed 1 year ago.
Navigating from view A to view B, i m passing a var show . View A isn t visible anymore .
I need to have a #State var stateShow whose initial value is the same as value show passed to B from A .
How to do it without using onAppear ? Thanx
struct B : View {
public var show:Bool
#State private stateShow : Bool
init (_ show : Bool){
self.show = show
}
}

init (_ show : Bool){
self.show = show
_stateShow = State(initialValue: show)
}
Answer credited to vadian ,another similar question was asked with a similar answer but slightly different conditions :

Related

In SwiftUI, how to create a non View object only once inside a child View

I want to link to a view that contains a non-view object - created once per user tap of the "Start" link - that is dependent on data selected by the user. The code below is as close as I've gotten. QuestionView.init is called as soon as HomeView appears, and again every time you select a new value for Highest Number, thus creating the Question object repeatedly, which is what I'm trying to avoid. I want to only create the Question object one time - when the user taps on the "Start" link.
I've tried many different approaches. It feels like I am stuck problem solving from an imperative UI oriented approach, instead of the new-for-me declarative approach of SwiftUI. Perhaps there's a bridge I'm missing from the state-driven approach of Views to the more familiar-to-me non-view objects, like my Question object I want to create only once.
struct Question {
let value1: Int
let value2: Int
let answer: Int
init(_ highestNumber: Int) {
print("Question.init \(highestNumber)")
value1 = Int.random(in: 1...highestNumber)
value2 = Int.random(in: 1...highestNumber)
answer = value1 * value2
}
var prompt: String {
"\(value1) x \(value2) = ?"
}
}
struct HomeView: View {
#State var highestNumber: Int = 12
var body: some View {
NavigationStack {
Picker("Highest Number", selection: $highestNumber) {
ForEach(4...12, id: \.self) { Text(String($0)) }
}
.pickerStyle(.wheel)
NavigationLink(destination: QuestionView(highestNumber: $highestNumber)) {
Text("Start")
}
}
}
}
struct QuestionView: View {
#Binding var highestNumber: Int
#State var question: Question
init(highestNumber: Binding<Int>) {
print("QuestionView.init")
self._highestNumber = highestNumber
question = Question(highestNumber.wrappedValue)
}
var body: some View {
Text(question.prompt)
Button("I got it") {
question = Question(highestNumber)
}
}
}

Cannot use instance member '' within property initializer OnAppear does not working as well

I am trying to create a LazyVGrid based on user selection in another view. As follows, the peoplelist and selectedpersonID are coming from other view.
I understand that I cannot use the "selectedpersons" during initializing of this view. I looked here(Cannot use instance member 'service' within property initializer; property initializers run before 'self' is available) to use onAppear() of the LazyVGrid.
It goes well during compiling and works ok if you select 1 person.
Once I selected 2 persons, I got a Fatal error that Index out of range at row.
struct Someview: View {
#ObservedObject var peoplelist : PersonList
let selectedpersonID : Set<UUID>?
#State private var days : [String] = Array(repeating: "0", count: selectedpersons.count * 5) //got first error here, during compiling
var body: some View {
VStack{
LazyVGrid(columns: columns) {
Text("")
ForEach(1..<6) { i in
Text("\(i)").bold()
}
ForEach(0..< selectedpersons.count , id: \.self) { row in
Text(selectedpersons[row].Name)
ForEach(0..<5) { col in
TextField("", text: $days[row * 5 + col])
}
}
}
.onAppear(){
days = Array(repeating: "0", count: selectedpersons.count * 5)}//no problem during compiling, but will have error when more than 1 person are selected.
.padding()
}
}
var selectedpersons: [Persons] {
return peoplelist.persons.filter {selectedpersonID!.contains($0.id)}
}
}
It seems to me that this OnAppear() is still slower than the content inside the LazyVGrid? So, the days is not changed quick enough for building the content insider the LazyVGrid?
Or did I make an error of the index in the array of days?
It's crashing because ForEach isn't a for loop its a View that needs to be supplied Identifiable data. If you're using indices, id: \self or data[index] then something has gone wrong. There are examples of how to use it correctly in the documentation.
Also onAppear is for performing a side-effect action when the UIView that SwiftUI manages appears, it isn't the correct place to set view data, the data should be already in the correct place when the View struct is init. Making custom Views is a good way to solve this.

Wrong counting while using ForEach in SwiftUI

I have found this question while scouring for some info for my project. It was unanswered, but I used it as an answer to my problem after a little fix '^^
The question:
how to change from default "2 cups" to 1 cup. ? My coffeAmmount is 1 so as I understand $0 should also be 1 in Text view. But its showing default as 2. Can someone explain ?
thanks.!!
The code:
import SwiftUI
struct ContentView: View {
#State private var coffeAmmount = 1
var body: some View {
Section(header: Text("Daily coffe intake")
.font(.headline)){
Picker("How many cups", selection: $coffeAmmount){
ForEach(1..<21){
Text($0 > 1 ? "\($0) cups" : "\($0) cup")
}
}
}
}
}
The original topic:
https://www.hackingwithswift.com/forums/100-days-of-swiftui/betterrest-day-28-challenge-2-how-to-create-a-simple-integer-picker/587
The other fix would be to make it an array, and not a range. This may be a bit more intuitive for some as your are always dealing with the numbers you actually set in the ForEach like this:
struct CoffeeView: View {
#State private var coffeAmmount = 1
var body: some View {
Picker("How many cups", selection: $coffeAmmount){
// make this an Array and the value matches
ForEach(Array(1..<21), id: \.self){ cup in
Text(cup > 1 ? "\(cup) cups" : "\(cup) cup")
}
}
}
}
You just have to set
coffeAmmount
to 0:
#State private var coffeAmmount = 0
SwiftUI treats this variable as a pointer to the place in range you created with use of ForEach.
So the ForEach range is (1, 2, 3, ..., 20) but their indexes are 0 for 1; 1 for 2 etc. Swift, as many of the programming languages, starts counting the indexes and other stuff from 0 not from 1 by default.
By setting coffeAmmount to 0 you actually show Xcode that you want the value from the index (place in range you created) "branded" 0 which value is 1 :)
Hope that helps people with the similar problem, since the question was asked 2 years ago.

ForEach - Index out of range?

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.

SwiftUI UIViewRepresentable UITextView Binding

Multiline text input is currently not natively supported in SwiftUI (hopefully this feature is added soon!) so I've been trying to use the combine framework to implement a UITextView from UIKit which does support multiline input, however i've been having mixed results.
This is the code i've created to make the Text view:
struct MultilineTextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.isScrollEnabled = true
view.isEditable = true
view.isUserInteractionEnabled = true
view.backgroundColor = UIColor.white
view.textColor = UIColor.black
view.font = UIFont.systemFont(ofSize: 17)
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
func frame(numLines: CGFloat) -> some View {
let height = UIFont.systemFont(ofSize: 17).lineHeight * numLines
return self.frame(height: height)
}
func makeCoordinator() -> MultilineTextView.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: MultilineTextView
init(_ parent: MultilineTextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
}
I've then implemented it in a swiftUI view like:
MultilineTextView(text: title ? $currentItem.titleEnglish : $currentItem.pairArray[currentPair].english)//.frame(numLines: 4)
And bound it to a state variable:
#State var currentItem:Item
It sort of works. However, the state var currentItem:Item contains an array of strings which I'm then cycling through using buttons which update the string array based on what has been inputted into MultilineTextView. This is where i'm encountering a problem where the MultilineTextView seems to bind to only the first string item in the array, and then it won't change. When I use swiftUI's native TextField view this functionality works fine and I can cycle through the string array and update it by inputting text into the TextField.
I think I must be missing something in the MultilineTextView struct to allow this functionality. Any pointers are gratefully received.
Update: Added model structs
struct Item: Identifiable, Codable {
let id = UUID()
var completed = false
var pairArray:[TextPair]
}
struct TextPair: Identifiable, Codable {
let id = UUID()
var textOne:String
var textTwo:String
}
Edit:
So I've done some more digging and I've found what I think is the problem. When the textViewDidChange of the UITextView is triggered, it does send the updated text which I can see in the console. The strange thing is that the updateUIView function then also gets triggered and it updates the UITextView's text with what was in the binding var before the update was sent via textViewDidChange. The result is that the UITextview just refuses to change when you type into it. The strange thing is that it works for the first String in the array, but when the item is changed it won't work anymore.
It appears that SwiftUI creates two variants of UIViewRepresentable, for each binding, but does not switch them when state, ie title is switched... probably due to defect, worth submitting to Apple.
I've found worked workaround (tested with Xcode 11.2 / iOS 13.2), use instead explicitly different views as below
if title {
MultilineTextView(text: $currentItem.titleEnglish)
} else {
MultilineTextView(text: $currentItem.pairArray[currentPair].textOne)
}
So I figured out the problem in the end, the reason why it wasn't updating was because I was passing in a string which was located with TWO state variables. You can see that in the following line, currentItem is one state variable, but currentPair is another state variable that provides an index number to locate a string. The latter was not being updated because it wasn't also being passed into the multiline text view via a binding.
MultilineTextView(text: title ? $currentItem.titleEnglish : $currentItem.pairArray[currentPair].english)
I thought initially that passing in one would be fine and the parent view would handle the other one but this turns out not to be the case. I solved my problem by making two binding variables so I could locate the string that I wanted in a dynamic way. Sounds stupid now but I couldn't see it at the time.