Change Text color on tap in SwiftUI - ios

I have defined a List of Text inside SwiftUI in the following way.
struct SampleView: View {
private let list: [String] = ["Tapping each line should change its color from grey to black", "First Line", "Second Line", "Third Line."]
var body: some View {
ScrollViewReader { value in
List {
ForEach(Array(zip(list, list.indices)), id: \.0) { eachString, index in
Text(eachString)
.bold().font(.largeTitle)
.foregroundColor(.gray)
.onTapGesture {
self.foregroundColor(.black)
}
}
}.listStyle(.plain)
}.frame(maxHeight: .infinity)
}
}
struct SampleView_Preview: PreviewProvider {
static var previews: some View {
SampleView()
}
}
I need to be able to change the foregroundColor of a particular TextView whenever the user taps on it.
CustomString is a class that I defined that conforms to Hashable. I can't get it to conform to ObservableObject at the same time which means I can't change its value inside the onTapGesture block.
I also can't remove the index from the loop as I'll need that too.
TIA.

Don't take it personal at all, just keep in mind that Stackoverflow is a great way to share between developers, but all the basics are much more clear in the documentation and samples from Apple. They are incredibly well done by the way.
https://developer.apple.com/tutorials/swiftui
And for your problem, you must declare a state variable with your color, so when the color changes, the view is regenerated.
I have not tested your code, because you don't provide a copy/paste ready to run playground or view class, but you see the principle.
One sure thing is that if you need each text cell to update independently, you should make a "TextCell" subview or whatever, that has it's own color. Not at the highest level. In your code, I presume all cells will get the same color, since it is defined at top level of your List.
Happy new year :)
...
#State var color: Color = .gray
var myString: [CustomString]
...
ScrollViewReader { value in
List {
ForEach(Array(zip(myString, myString.indices)), id: \.0) { eachString, index in
Text(eachString.text)
.foregroundColor(color)
.onTapGesture {
self.color = .black
}
}
}

Related

Why is this SwiftUI state not updated when passed as a non-binding parameter?

Here's what I want to do:
Have a SwiftUI view which changes a local State variable
On a button tap, pass that variable to some other part of my application
However, for some reason, even though I update the state variable, it doesn't get updated when it's passed to the next view.
Here's some sample code which shows the problem:
struct NumberView: View {
#State var number: Int = 1
#State private var showNumber = false
var body: some View {
NavigationStack {
VStack(spacing: 40) {
// Text("\(number)")
Button {
number = 99
print(number)
} label: {
Text("Change Number")
}
Button {
showNumber = true
} label: {
Text("Show Number")
}
}
.fullScreenCover(isPresented: $showNumber) {
SomeView(number: number)
}
}
}
}
struct SomeView: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
If you tap on "Change Number", it updates the local state to 99. But when I create another view and pass this as a parameter, it shows 1 instead of 99. What's going on?
Some things to note:
If you uncomment Text("\(number)"), it works. But this shouldn't be necessary IMO.
It also works if you make SomeView use a binding. But for my app, this won't work. My actual use case is a 'select game options' view. Then, I will create a non-SwiftUI game view and I want to pass in these options as parameters. So, I can't have bindings all the way down my gaming code just because of this bug. I want to just capture what the user enters and create a Parameters object with that data.
It also works if you make it a navigationDestination instead of a fullScreenCover. ¯\(ツ)/¯ no idea on that one...
A View is a struct, therefore its properties are immutable, so the view can not change its own properties. This is why changing the property named number from inside the body of the view needs this property to be annotated with a #State property wrapper. Thanks to Swift and SwiftUI, transparent read and write callbacks let the value being seen changed. So you must not pass number as a parameter of SomeView() when calling fullScreenCover(), but pass a reference to number, for the callbacks to be systematically called: $number. Since you are not passing an integer anymore to construct struct SomeView, the type of the property named number in this struct can not any longer be an integer, but must be a reference to an integer (namely a binding): use the #Binding annotation for this.
So, replace SomeView(number: number) by SomeView(number: $number) and let number: Int by #Binding var number: Int to do the job.
Here is the correct source code:
import SwiftUI
struct NumberView: View {
#State var number: Int = 1
#State private var showNumber = false
var body: some View {
NavigationStack {
VStack(spacing: 40) {
// Text("\(number)")
Button {
number = 99
print(number)
} label: {
Text("Change Number")
}
Button {
showNumber = true
} label: {
Text("Show Number")
}
}
.fullScreenCover(isPresented: $showNumber) {
SomeView(number: $number)
}
}
}
}
struct SomeView: View {
#Binding var number: Int
var body: some View {
Text("\(number)")
}
}
After all that said to obtain a valid source code, their is a little trick that has not been explained up to now: if you simply replace in your source code Text("Change Number") by Text("Change Number \(number)"), without using $ reference nor #Binding keywords anywhere, you will see that the problem is also automatically solved! No need to use #binding in SomeView! This is because SwiftUI makes optimizations when building a tree of views. If it knows that the displayed view changed (not only its properties), it will compute the view with updated #State values. Adding number to the button label makes SwiftUI track changes of the number state property and it now updates its cached value to display the Text button label, therefore this new value will be correctly used to create SomeView. All of that may be considered as strange things, but is simply due to optimizations in SwiftUI. Apple does not fully explain how it implements optimizations building a tree of views, there are some informations given during WWDC events but the source code is not open. Therefore, you need to strictly follow the design pattern based on #State and #Binding to be sure that the whole thing works like it should.
All of that said again, one could argue that Apple says that you do not have to use #Binding to pass a value to a child view if this child view only wants to access the value: share the state with any child views that also need access, either directly for read-only access, or as a binding for read-write access (https://developer.apple.com/documentation/swiftui/state). This is right, but Apple says in the same article that you need to place [state] in the highest view in the view hierarchy that needs access to the value. With Apple, needing to access a value means that you need it to display the view, not only to do other computations that have no impact on the screen. This is this interpretation that lets Apple optimize the computation of the state property when it needs to update NumberView, for instance when computing the content of the Text("Change Number \(number)") line. You could find it really tricky. But there is a way to understand that: take the initial code you wrote, remove the #State in front of var number: Int = 1. To compile it, you need to move this line from inside the struct to outside, for instance at the very first line of your source file, just after the import declaration. And you will see that it works! This is because you do not need this value to display NumberView. And thus, it is perfectly legal to put the value higher, to build the view named SomeView. Be careful, here you do not want to update SomeView, so there is no border effects. But it would not work if you had to update SomeView.
Here is the code for this last trick:
import SwiftUI
// number is declared outside the views!
var number: Int = 1
struct NumberView: View {
// no more state variable named number!
// No more modification: the following code is exactly yours!
#State private var showNumber = false
var body: some View {
NavigationStack {
VStack(spacing: 40) {
// Text("\(number)")
Button {
number = 99
print(number)
} label: {
Text("Change Number")
}
Button {
showNumber = true
} label: {
Text("Show Number")
}
}
.fullScreenCover(isPresented: $showNumber) {
SomeView(number: number)
}
}
}
}
struct SomeView: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
This is why you should definitely follow the #State and #Binding design pattern, taking into account that if you declare a state in a view that does not use it to display its content, you should declare this state as a #Binding in child views even if those children do not need to make changes to this state. The best way to use #State is to declare it in the highest view that needs it to display something: never forget that #State must be declared in the view that owns this variable; creating a view that owns a variable but that does not have to use it to display its content is an anti-pattern.
Since number isn't read in body, SwiftUI's dependency tracking detect it. You can give it a nudge like this:
.fullScreenCover(isPresented: $showNumber) { [number] in
Now a new closure will be created with the updated number value whenever number changes. Fyi the [number] in syntax is called a "capture list", read about it here.
Nathan Tannar gave me this explanation via another channel which I think gets to the crux of my problem. It does seem that this is a SwiftUI weirdness caused by knowing when and how it updates views based on state. Thanks Nathan!
It’s because the number isn’t “read” in the body of the view. SwiftUI is smart in that it only triggers view updates when a dependency of the view changes. Why this causes issues with the fullScreenCover modifier is because it captures an #escaping closure for the body. Which means it’s not read until the cover is presented. Since its not read the view body will not be re-evaluated when the #State changes, you can validate this by setting a breakpoint in the view body. Because the view body is not re-evaluated, the #escaping closure is never re-captured and thus it will hold a copy of the original value.
As a side note, you’ll find that once you present the cover for the first time and then dismiss, subsequent presentations will update correctly.
Arguably this seems like a SwiftUI bug, the fullScreenCover probably shouldn’t be #escaping. You can workaround by reading the number within the body, or wrapping the modifier with something like this, since here destination is not #escaping captured so the number will be read in the views body evaluation.
struct FullScreenModifier<Destination: View>: ViewModifier {
#Binding var isPresented: Bool
#ViewBuilder var destination: Destination
func body(content: Content) -> some View {
content
.fullScreenCover(isPresented: $isPresented) {
destination
}
}
}

SwiftUI View is reinitialized when #Binding Property has not been modified

I am executing a SwiftUI playground that contains 2 labels and 2 buttons that modified the value of these labels.
I've stored the value of these labels in a #ObservableObject. Whene I modify the value of any of these properties, both views CustomText2 and CustomText3 are reinitialized, even the one that his values has not changed.
Code:
final class ViewModel: ObservableObject {
#Published var title: Int
#Published var title2: Int
init(title: Int = 0, title2: Int = 0) {
self.title = title
self.title2 = title2
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Button(
action: {
viewModel.title += 1
}, label: {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
}
)
CustomText1(
title: $viewModel.title
)
Button(
action: {
viewModel.title2 += 1
}, label: {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
}
)
CustomText2(
title: $viewModel.title2
)
}
.padding()
}
}
struct CustomText1: View {
#Binding var title: Int
init(
title: Binding<Int>
) {
self._title = title
}
var body: some View {
Text("\(title)")
.foregroundColor(.black)
}
}
However if I store both properties as #State in the view and I modify them, the CustomTexts are not reinitialized, they just update their value in the body without executing an init.
Why are they getting reinitialized when I store both properties in the ViewModel?
I've tried to make the views conforming Equatable but they're reinitialized.
Can be a performance problem if the views are initialized many times?
I am interested in not having the subviews reinitialized because I want to perform custom stuff in the init of some subviews.
When you have one StateObject that encompasses multiple State variables, change in one will redraw the entire view. In your case, any change in any variable in viewModel will trigger the publisher of viewModel and reload ContentView
Also we are not supposed to make any assumptions on when a View will be redrawn, as this might change with different versions of SwiftUI. Its better to move this custom stuff you are doing in the init of views to some other place(if it can be). Init should only do work needed to redraw the view with the new state parameters and nothing else.
#ObservableObject is for model data, not view data.
The reason is when using lets or #State vars, SwiftUI uses dependency tracking to decide if body needs to be called and in your case body doesn't use the values anywhere so there is no need to call it.
It can't track objects in the same way, if there is a #StateObject declared then body is called regardless if any properties are accessed, so it's best to start with #State value types and only change to #StateObject when you really need features of a reference type. Not very often now we have .task which is the place to put your custom async work.

SWIFTUI TextView with real-time, programmatically-appendable text

I'm looking for a way to create a simple textview (command line like) that will show the user what is going on inside the app. It should be able to append elements programmatically in real time so user can monitor and copy text from it. Im writing app for macOS.
So far i done this:
class OutputElementStruct: Identifiable
{
var HEX: String
var Balance: String
init(HEX: String, Balance: String)
{
self.HEX = HEX
self.Balance = Balance
}
}
class OutputElements: ObservableObject
{
#Published var OutputElement = [OutputElementStruct]()
init()
{
self.OutputElement =
[
OutputElementStruct(HEX: "sometext", Balance: "sometext")
]
}
}
Then it shows to user
struct OutputView: View
{
#StateObject var OutputArray = OutputElements()
var body: some View
{
GeometryReader
{ Geo in
ZStack
{
BoxView(width: 600, height: 700, Name: "")
VStack
{
List
{
ForEach($OutputArray.OutputElement)
{ item in
Text(verbatim: item.HEX.wrappedValue)
}
}
}
}
}
.frame(width: 600, height: 700, alignment: .center)
}
}
and here is data added.
#StateObject var OutputArray = OutputElements()
.onTapGesture
{
print("button tapped")
OutputArray.OutputElement.append(OutputElementStruct(HEX: "sometext", Balance: "sometext"))
}
It is working, but not in real time. While i tapped the button it is prints "button tapped" in Xcode, but text view does not displays anything except firstly added item in init(). So how to make it work?
While i tapped the button it is prints "button tapped" in Xcode, but text view does not displays anything except firstly added item in init(). So how to make it work?
It looks like you've got two separate arrays that are both named OutputArray, and the one you're appending objects to isn't the one that your view is displaying. That's why you don't see any new objects show up even though you know you're adding them.
You need a single array that's used in both places. I think you've got the right idea by using #StateObject, but instead of creating two #StateObject variables, create just one and add it to the environment as explained in the StateObject documentation.

SwiftUI replace and copy text TextEditor

I have read countless articles and watched 10 hours of YouTube videos and still can't figure this out, maybe I'm just missing the term. I'm old school and still trying to learn this backwards swift stuff compared to vb or javascript.
I am using the default Mac OS document template in Xcode 13.1. it comes with a dialogue to open a file then dumps it into the TextEditor as utf8 string.
For my part I added another TextEditor and a button but keep getting error when I try to copy the text from the original texteditor to the new texteditor when button is pressed.
struct ContentView: View {
#Binding var document: test2Document
#State var newtexteditor: String = "press the button"
var body: some View {
VStack{
//main text editor
TextEditor(text: $document.text)
//divider
Divider()
//new text editor
TextEditor(text: $newtexteditor).padding()
//button
Button("test", action: dosomething)
}
}
func dosomething () {
$newtexteditor = $document
}
}
Also, is there any ways of giving objects a name or label and using that to say things like textbox1.text = textbox2.text like you could using storyboards. I obviously know that's not swift syntax but you get the point.
#State and #Binding vars are special kinds of variables. They actually contain 2 sub-variables: projectedValue and wrappedValue.
projectedValue is like a reference to the actual value, and allows the #State/#Binding to be modified from further down the view hierarchy.
You access this by saying $newTextEditor or $document.text.
This is only used for passing references down the view hierarchy, where you can later set the wrappedValue.
wrappedValue is the actual value — in your case, a String.
You access this by just saying newTextEditor or document.text. No $.
You're able to set this - for example, newTextEditor = "New text" or newTextEditor = document.text. The UI will automatically update to reflect the changes.
Here's an example:
struct ContentView: View {
#State var text = "Hello"
var body: some View {
VStack {
Text("The text is: \(text)") /// display the `wrappedValue`
SubView(text: $text) /// pass in the `projectedValue`
}
}
}
struct SubView: View {
#Binding var text: String
var body: some View {
Button("Click me to change text") {
text = "New text" /// set the `wrappedValue`
}
}
}
In your code, it would look something like this:
struct ContentView: View {
#Binding var document: Test2Document /// Side Note: structs like `Test2Document` should be Capitalized
#State var newTextEditor: String = "press the button" /// also use camel case
var body: some View {
VStack{
TextEditor(text: $document.text) /// pass in the `projectedValue`
Divider()
TextEditor(text: $newTextEditor) /// pass in the `projectedValue`
.padding()
Button("test", action: dosomething)
}
}
func dosomething() {
newTextEditor = document.text /// set the `wrappedValue`
}
}

How can I dynamically add SwiftUI Views to a parent View?

Is it possible to add multiple SwiftUI Views to a parent View dynamically & programmatically?
For example suppose we have a basic View such as:
struct MyRectView: View {
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
}
}
And a Button defined as:
struct MyButtonThatMakesRects: View {
var body: some View {
Button(
action: {
// Create & add Views to parent here?
// ...
},
label: {
Text("tap to create a Rect")
}
)
}
}
Is there any way I can create multiple instances of MyRectView in a parent View when MyButtonThatMakesRects is tapped?
My initial thinking was in line with how I would do this in UIKit. That being on button tap, create a new UIView(), and then use .addSubview(...) to add it to a parent. Not sure if SwiftUI has similar functionality. Or maybe there is a simpler way to do this that I'm not seeing?
SwiftUI is functional and reactive, so its output is entirely a reflection of state. You'll have to store and manipulate state that results in a SwiftUI view with your desired outcome. The view is reconstructed from scratch every time its state changes. (Not really, as there's some efficient diffing under the hood, but it's a good mental model to use.)
The simplest way that SwiftUI provides is the #State property wrapper, so a version of what you're asking for would look something like this:
struct RootView: View {
#State private var numberOfRects = 0
var body: some View {
VStack {
Button(action: {
self.numberOfRects += 1
}) {
Text("Tap to create")
}
ForEach(0 ..< numberOfRects, id: \.self) { _ in
MyRectView()
}
}
}
}
I'm guessing your desired end result is more complicated than that, but you can use #State or use a property pointing to a separate class that handles your state/model, marked with the #ObservedObject wrapper, to get to whatever you need.

Resources