SWIFTUI TextView with real-time, programmatically-appendable text - ios

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.

Related

Change Text color on tap in SwiftUI

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
}
}
}

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`
}
}

Got rare behaviour in SwiftUI with checking boolean in ForEach with Struct elements in Array

I'm trying to understand why this code is working sometimes as expected but sometimes not. Im showing a List of Boxes that can be selected by a simple tap. When a user tapped a box, a Text changes.
I have a struct and a class:
struct SelectionBoxItemStruct: Identifiable
{
var id = UUID()
var title: String
var subtitle: String
var selected = false
}
class SelectionBoxItemClass: ObservableObject {
#Published var selectionBoxItems = [SelectionBoxItemStruct(title: "XXX", subtitle: "YYY"), SelectionBoxItemStruct(title: "XXX", subtitle: "YYY")]
}
In the main View I declared the class like this:
#ObservedObject var selectionBoxItemClass = SelectionBoxItemClass()
And later in a ForEach I do this:
ForEach($selectionBoxItemClass.selectionBoxItems) { $selection in
Text(selection.selected ? "True" : "False")
Rectangle() // Example of a Box
.frame(width: geometry.size.width/1.2, height: 100)
.onTapGesture(perform: {
selection.selected.toggle()
print(selection.selected)
})
}
It will print correctly after every tap "true, false, true, ...", but the displayed "TextView" shows only sometimes the right Text and changes, but sometimes only the correct value is printed but the Text doesn't change. In conclusion: every build works different.

SwiftUI - trying to identify and pass the value of the list item being selected

I'm following the Apple Developer tutorial on interfacing with UIKIt
The only real difference is that that I'm trying to call
PageView<Page:View> {
from a NavigationLink. The example code within PageView has a default value hardcoded of
#State var currentPage = 0
I'd rather initiate the swipe process on the view that the user selected and not start at the first one and I'm trying to create a simple way to swipe through the items without having to return to the navigationlink to access the next item on the list.
Is there a way to trap the index of the link the user selected and pass this through to PageView as a variable.
I'd tried incrementing a counter and using a function to increment the counter and return the PageView sruct without any success.
animals is an array, just in case that wasn't clear and I'm using the map function to pass in the appropriate value to create the detailed view I'm looking to create . Also, I realize I could probably put the detail view in a scroll view.
Here's my code
var body: some View {
List {
ForEach(collection.animals, id: \.self) { animal in
NavigationLink(destination:PageView(self.collection.animals.map { AnimalDetail(animalMetadata: $0.wrappedMetadata )})) {
AnimalRow(thumbnailImage: Image(uiImage: UIImage(data: animal.image!)!), label: (animal.name!) ).font(.subheadline)
}
}.onDelete(perform: delete)
}
Everything else seems to work fine - just can't figure a way to capture an index value from the list to indicate which item/view was clicked and to pass that value through to PageView in order to create this as the first viewed View.
Thank you !
edit
I tried using .enumerated() on the Array but still couldn't figure out how to get the specific index value to the PAgeView view.
List {
ForEach(Array(collection.animals.enumerated()), id: \.1.id) {index, item in
NavigationLink(destination:PageView( self.collection.animals.map { AnimalDetail(animalMetadata: $0.wrappedMetadata)} )) {
AnimalRow(thumbnailImage: Image(uiImage: UIImage(data: item.image!)!), label: (animal.name!) ).font(.subheadline)
}
}.onDelete(perform: delete)
Try the following...
In PageView:
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
#State var currentPage: Int // no initial value
init(_ views: [Page], selection: Int) {
_currentPage = State(initialValue: selection)
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
// ... other code here
}
In your code:
var body: some View {
List {
ForEach(Array(collection.animals.enumerated()), id: \.element) { idx, animal in
NavigationLink(destination:
PageView(self.collection.animals.map {
AnimalDetail(animalMetadata: $0.wrappedMetadata )},
selection: idx)) { // << here !!
AnimalRowRow(thumbnailImage: Image(uiImage: UIImage(data: animal.image!)!),
label: (animal.name!) ).font(.subheadline)
}
}.onDelete(perform: delete)
}

SwiftUI, how to bind EnvironmnetObject Int property to TextField?

I have an ObservableObject which is supposed to hold my application state:
final class Store: ObservableObject {
#Published var fetchInterval = 30
}
now, that object is being in injected at the root of my hierarchy and then at some component down the tree I'm trying to access it and bind it to a TextField, namely:
struct ConfigurationView: View {
#EnvironmnetObject var store: Store
var body: some View {
TextField("Fetch interval", $store.fetchInterval, formatter: NumberFormatter())
Text("\(store.fetchInterval)"
}
}
Even though the variable is binded (with $), the property is not being updated, the initial value is displayed correctly but when I change it, the textfield changes but the binding is not propagated
Related to the first question, is, how would I receive an event once the value is changed, I tried the following snippet, but nothing is getting fired (I assume because the textfield is not correctly binded...
$fetchInterval
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.sink { interval in
print("sink from my code \(interval)")
}
Any help is much appreciated.
Edit: I just discovered that for text variables, the binding works fine out of the box, ex:
// on store
#Published var testString = "ropo"
// on component
TextField("Ropo", text: $store.testString)
Text("\(store.testString)")
it is only on the int field that it does not update the variable correctly
Edit 2:
Ok I have just discovered that only changing the field is not enough, one has to press Enter for the change to propagate, which is not what I want, I want the changes to propagate every time the field is changed...
For anyone that is interested, this is te solution I ended up with:
TextField("Seconds", text: Binding(
get: { String(self.store.fetchInterval) },
set: { self.store.fetchInterval = Int($0.filter { "0123456789".contains($0) }) ?? self.store.fetchInterval }
))
There is a small delay when a non-valid character is added, but it is the cleanest solution that does not allow for invalid characters without having to reset the state of the TextField.
It also immediately commits changes without having to wait for user to press enter or without having to wait for the field to blur.
Do it like this and you don't even have to press enter. This would work with EnvironmentObject too, if you put Store() in SceneDelegate:
struct ContentView: View {
#ObservedObject var store = Store()
var body: some View {
VStack {
TextField("Fetch interval", text: $store.fetchInterval)
Text("\(store.fetchInterval)")
}
} }
Concerning your 2nd question: In SwiftUI a view gets always updated automatically if a variable in it changes.
how about a simple solution that works well on macos as well, like this:
import SwiftUI
final class Store: ObservableObject {
#Published var fetchInterval: Int = 30
}
struct ContentView: View {
#ObservedObject var store = Store()
var body: some View {
VStack{
TextField("Fetch interval", text: Binding<String>(
get: { String(format: "%d", self.store.fetchInterval) },
set: {
if let value = NumberFormatter().number(from: $0) {
self.store.fetchInterval = value.intValue
}}))
Text("\(store.fetchInterval)").padding()
}
}
}

Resources