Making items in HStack visible / invisible later - ios

I have a game view that has a board and I have several parts in a VStack and the final 2 HStack are some 'buttons' and then a row of tiles for the game.
VStack(spacing: 5) {
HStack {}
HStack {}
......
......
HStack {
ResetButton {
self.returnLetters()
}
NewLine {
self.newLine()
}
CalcButton {
self.calcButton()
}
StartButton {
self.startButton()
}
}
HStack {
ForEach(0..<7) { number in
Letter(text: self.tray[number], index: number, onChanged: self.letterMoved, onEnded: self.letterDropped)
}
}
This sets the screen all very well However I would ideally not want to show the Start and Calc buttons until later in the game, or indeed replace the Reset and NewLine buttons with Start and Calc.
As it looks
Ideally What I would have until later in game
Changing to this on the last go
Is there a way to not show items in the Stack or add items to the stack later please?
thanks

Use #State boolean variable that define if the buttons should be shown or not, when you change their value the view will be refreshed.
#State var isResetVisible = true
...
if isResetVisible {
ResetButton {
self.returnLetters()
}
}
then you set it to false somewhere and it will hide

Here is a pattern, introduce corresponding #State variable with desired initial state value. The state can be changed later at any time somewhere in view and view will be updated automatically showing conditionally dependent button:
#State private var newLineVisible = false // initially invisible
...
var body: some View {
...
HStack {
ResetButton {
self.returnLetters()
}
if self.newLineVisible { // << include conditionally
NewLine {
self.newLine()
}
}
CalcButton {
self.calcButton()
}

Related

How to know if the first item in the list that is inside of a ForEach loop is visible in SwiftUI?

I have a list with a ForEach loop for items. I would like to place a button on the side of the list that would pull the list to the top if the list is not currently at the top.
The key is that the button should only be visible if the list is currently not at the top position, therefore, I need some way to get the position of the list or track the top element in ForEach loop to know when it is in view to trigger the show button event.
I believe the following gives the behaviour you are looking for. Note that I took the liberty of disabling the "Scroll to Top" button, rather than hiding it, because the hiding causes layout changes that are troublesome.
The gist of it is to use GeometryReaders to compare the top of the list (in the global coordinate system) to the bottom of the first list entry (in the same coordinate system) and enable the button when the list entry is entirely above the top of the list.
The line of the form let _ = { ... }() allows us to inject some code into the declarative code structure SwiftUI uses.
The listProxy.scrollTo(1) call will cause the list to scroll to the position that makes the list item marked with .id(1).
struct ScrollButtonTest: View {
#State private var listTop: CGFloat = 0.0
#State private var firstRowBottom: CGFloat = 0.0
var body: some View {
VStack {
ScrollViewReader { listProxy in
Text("listTop: \(listTop)")
Text("firstRowBottom: \(firstRowBottom)")
Button("Scroll to Top") {
listProxy.scrollTo(1)
}.disabled(listTop < firstRowBottom )
GeometryReader { outerGP in
List {
// We set up the first item manually so that
// we can wrap it in a GeometryReader
GeometryReader { innerGP in
let _ = {
listTop = outerGP.frame(in: .global).origin.y
firstRowBottom = innerGP.frame(in: .global).origin.y + innerGP.frame(in: .global).height
}()
Text("Item 1")
}.id(1)
ForEach(2..<99) { index in
Text("Item \(index)").id(index)
}
}
}
}
}
}
}

Is there a way to detect only the cells displayed on the screen when using List in SwiftUI?

Is it possible to detect the cell (or array position) of the List currently displayed on the View screen from among the 100 Items as shown below?
Also, at that time, I would like to get the position of the cell in detail, such as some cells that are half hidden from the screen at the top and bottom.
struct DemoList: View {
// 1.
#State private var items: [Item] = (0..<100).map { Item(title: "Item #\($0)") }
// 2.
var body: some View {
List {
ForEach(items) { item in
Text(item.title)
}
}
}
}
This might be a starting point. But beware that List always keeps one "buffer" cell before .onAppear/.onDisappear kick in.
List {
ForEach(items) { item in
GeometryReader { geo in
Text(item.title)
.onAppear {
print(item.title, geo.frame(in: .global))
}
.onDisappear {
print(item.title, "---")
}
}
}
}
A more sophisticated approach would use PreferenceKey. I recommend this (look for "GridInfoPreference"): https://swiftui-lab.com/impossible-grids/
Try using ‘onAppear’ modifier as Text(text).onAppear {}
Since iOS 15 there have been improvements around that modifier

Focused TextField ends up covered by the keyboard in SwiftUI

What I'm trying to accomplish
A vertical list of TextField views that user enters values in one at a time. After entering a value in a text field and pressing keyboard submit button the focus moves to the next text field.
The problem I'm facing
The keyboard obstructs the active text field. I was hoping that if I used ScrollView it would automagically scroll to the next focused text field, but that's not the case.
Then I decided to try using ScrollViewReader's ScrollViewProxy object to scroll to the focused text field, which works! But not quite. It scrolls to the selected text field, but not enough to go over the keyboard. It seems to always be just a little bit underneath the top of the keyboard (regardless of whether there's keyboard toolbar or autocorrection bar or nothing).
Here's a screenshot of roughly where the text field ends up after I call proxy.scrollTo(focusedInput):
Here's where I would want it to end up:
Minimal code sample
struct ContentView: View {
#State var inputsValues: [String]
#FocusState var focusedInput: Int?
init() {
inputsValues = (0..<30).map { _ in "" }
}
var body: some View {
ScrollViewReader { proxy in
ScrollView {
VStack {
ForEach((0..<inputsValues.count), id: \.self) { i in
TextField("Value", text: $inputsValues[i])
.focused($focusedInput, equals: i)
.submitLabel(.next)
.id(i)
.onSubmit {
if (i + 1) < inputsValues.count {
focusedInput = i + 1
proxy.scrollTo(focusedInput)
} else {
focusedInput = nil
}
}
}
}
}
}.toolbar {
ToolbarItem(placement: .keyboard) {
Text("This is toolbar")
}
}
}
}
Chaning two+ states in one closure (when they are related somehow or affects one area) are handled not very good (or not reliable). The fix is to separate them in different handlers.
Tested with Xcode 13.3 / iOS 15.4
Here is main part:
VStack {
// ... other code
.onSubmit {
// update state here !!
if (i + 1) < inputsValues.count {
focusedInput = i + 1
} else {
focusedInput = nil
}
}
}
}
.onChange(of: focusedInput) {
// react on state change here !!
proxy.scrollTo($0)
}
Complete test module is here

Are there any ways to copy gestures in SwiftUI or handle them and pass them down to child views?

I have two TextEditors next to each other and I want to synchronize their scroll state, for example, if I scroll on the left one, the right one should show the same.
//basic code
#State private var textLeft: String = "view left"
#State private var textRight: String = "view right"
var body: some View {
HStack() {
TextEditor(text: $textLeft)
TextEditor(text: $textLeft)
.gesture(DragGesture(coordinateSpace: .global)
.updating($gestureState, body: { (value, state, transaction) in
print(value.translation)
})
.onChanged { value in
//this blocks the TextEditor to scroll
//or won't be called if the gesture is handled by the editor
//
//if I apply the offset to the other Editor
//it results in weird looking behavior
print(offset)
}
)
}
}
I tried several things like applying a gesture to it and capture the movement and set it to the other as an .offset(value), but either the DragGesture blocks the TextEditor or the TextEditor blocks the DragGesture.
Are there any approaches like copying gestures and apply them to another view or pass through the .gesture() to the view itself?
Thanks for any help!
You can try extracting the Gesture to a computed property:
var dragGesture: some Gesture {
DragGesture(coordinateSpace: .global)
.updating($gestureState) { value, state, transaction in
print(value.translation)
}
.onChanged { value in
// this blocks the TextEditor to scroll
// or won't be called if the gesture is handled by the editor
//
// if I apply the offset to the other Editor
// it results in weird looking behavior
print(offset)
}
}
var body: some View {
HStack {
TextEditor(text: $textLeft)
.gesture(dragGesture)
TextEditor(text: $textLeft)
.gesture(dragGesture)
}
}

Dynamic view with swiftUI

I'm trying to make a dynamic view to answer some questions. Those questions are created from my back office, ant the next question depends on the current user answer. So I have to build my view as things progress.
Here is a example :
First Question :
Are you alive ? : | Are you sure ?
|
YES/NO (no chosen) | YES/NO
I made a subview with the different type of question, and a container view to embed the path like that :
Here is the code of my container View, the white block is embed in a Scrollview
#ObservedObject var VM:SurveyBrowserViewModel
#State private var offsetX:CGFloat = 0
#State private var opacity:Double = 1
#State var myView:AnyView = AnyView(EmptyView())
var body: some View {
ZStack{
Image("background_survey_canyon")
.resizable()
backgroundTriangle
.opacity(0.65)
ScrollView(showsIndicators:false){
myView
.padding(.top, 55 + safeAreaTop)
.padding(.bottom, 60 + safeAreaBottom)
.offset(x : offsetX , y: 0)
.opacity(opacity)
.onReceive(VM.nextQuestionPassthrough) { (v) in
self.myView = v
}
.onReceive(VM.nextQuestionValuePassthrough) { (v) in
self.offsetX = v
self.opacity = 0
withAnimation(.spring()) {
self.offsetX = 0
self.opacity = 1
}
}
}
.frame(maxWidth: .infinity,maxHeight: .infinity)
}
}
and here is the viewModel section which updates the View state, current question is the question which needs to be displayed according the good type.
var nextQuestionPassthrough = PassthroughSubject<AnyView,Never>()
/// Get The good view to match the correct question tyoe
/// - Parameter answerSelected: Binding bool to hide/show next button visibility
private func getGoodView(){
guard (currentQuestion != nil) else { nextQuestionPassthrough.send(AnyView(EmptyView())); return }
switch QuestionType.intToQuestionType(value: currentQuestion!.type!){
case .YesOrNo :
if currentQuestionViewModel == nil{
currentQuestionViewModel = YesNoQuestionViewModel(question: currentQuestion!,temporaryId: temporaryId)
}
nextQuestionPassthrough.send(AnyView(YesNoQuestionView(VM: currentQuestionViewModel as! YesNoQuestionViewModel,answerSelected: nextViewState! )))
}
}
I've tried different way to achieve what I'm expecting :
I want a animation like a auto scroll coming form right to left, but it's cosmetic and it's not my main problem.
My problem is that when i send view from the nextQuestionPassthrough the init section from my new view is called but not the appear one. So all state are saved as we can see on the picture ( yes and no answer are two #State set to false at initialization but stay selected when I send the new view. I tried several ideas, like a published view instead of a #State but the behavior is the same.
I'm thinking about a new approch, with my container embed two views to make the good animation and the good life cycle ( init then appear )
Has anyone ever studied this problem or done something similar?

Resources