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?
Related
I want to animate a Slider whenever its value changes with a custom animation (with a custom duration). But somehow it doesn't work. No matter what animation I specify, it always uses the same (very short) animation that is almost not noticeable.
Here's a minimal code example:
struct SliderView: View {
#State var value: Float = 0
var body: some View {
VStack {
Slider(value: $value)
.animation(.linear(duration: 3), value: value) // ← here 🎬
Button("Toggle") {
value = value < 0.5 ? 1 : 0
}
}
.frame(width: 300, height: 200)
.padding(16)
}
}
What am I doing wrong?
Note 1: Without the animation modifier, I get no animation at all and the knob immediately jumps to its new position. With the modifier, it animates but it doesn't seem to respect the concrete animation I specify.
Note 2: I know that I could use an explicit animation and wrap the change in a withAnimation closure. But even if that worked, it's not an option for me because the in my concrete case, the value is not a #State but a #Binding and is thus set externally from a view model that should not know anything about 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)
}
}
}
}
}
}
}
I'm new to the development of Swift. I've done several courses but I have some questions that I can't find.
I want to design an application that has 50 or more images, with a previous and next button to go through them in a matrix.
I've looked at random snippets of sample code, but I'm having a hard time figuring out how to do it. What's the easiest and most efficient method to go back and forth between these images in a view? The other problem I have is that I want to be able to save the image when it is selected.
Surely the answer to this is easy for you, but I'm a beginner, and I can't find a solution, after two days of searching and trying.
Thanks in advance!
Finotoni
There is some steps you need to do:
Get/ define your images
Have methods (or another way) of getting the previous and next image
Displaying the current image
Here is an really easy example that should help you understand:
struct ContentView: View {
// Your images
#State var images = ["image1", "image2", "image3"]
// Some indicator on what image is the current one
#State var imageIndicator: Int = 0
var body: some View {
HStack {
// Prev one
Button(action: {
// Call a method that gets you the previous image
if self.imageIndicator == 0 {
self.imageIndicator = self.images.count - 1
} else {
self.imageIndicator = self.imageIndicator - 1
}
}) {
Image(systemName: "arrow.left")
}
// Image with indicator to what image to display
Image(self.images[self.imageIndicator]).resizable().frame(width: 100, height: 100)
// Next one
Button(action: {
// Call a method that gets you the next image
if self.imageIndicator == self.images.count - 1 {
self.imageIndicator = 0
} else {
self.imageIndicator = self.imageIndicator + 1
}
}) {
Image(systemName: "arrow.right")
}
}
}
}
Produces this:
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()
}
My app has one button that produces either a green or red background. The user presses the button over and over to generate green or red each time and the results are recorded into an Observable Object called "Current Session." The current totals are displayed in a custom view made of capsules. For some reason, the view will not update the totals and keeps displaying 0.
I've tried adding a state binding for numberOfGreens in ResultsBar but that doesn't work either. It still just shows 0 every time. The environment has been added to all of my views. I've tried printing the results and the result numbers show up correct in the console but the UI will not update. Interestingly, the trialsRemaining variable works perfectly on other screens in my app, having no trouble updating when it has to. Also, ResultsBar works perfectly fine in preview.
class CurrentSession: ObservableObject {
#Published var trialsRemaining: Int = 100
#Published var numberOfGreens: Int = 0
#Published var numberOfReds: Int = 0
func generateGreenOrRed() {
...
if resultIsGreen {numberOfGreens += 1}
else if resultIsRed {numberOfReds += 1}
trialsRemaining -= 1
}
}
struct ResultsBar: View {
let totalBarWidth: CGFloat = UIScreen.main.bounds.width - 48
var onePercentOfTotalWidth: CGFloat { return totalBarWidth/100 }
var numberOfGreens: Int
var body: some View {
ZStack(alignment: .leading) {
Capsule()
.frame(width: totalBarWidth, height: 36)
.foregroundColor(.red)
.shadow(radius: 4)
Capsule()
.frame(width: (onePercentOfTotalWidth * CGFloat(numberOfGreens)), height: 36)
.foregroundColor(.green)
.shadow(radius: 4)
}
}
}
struct ResultsView: View {
#EnvironmentObject var session: CurrentSession
var body: some View {
VStack {
HStack {
Text("\(session.numberOfGreens)")
Spacer()
Text("\(session.numberOfReds)")
}
ResultsBar(numberOfGreens: session.numberOfGreens)
}
}
}
I am running Xcode 11.2 beta 2 (11B44) on macOS Catalina 10.15.2 Beta (19C32e).
Are you passing a reference to a CurrentSession instance to your ResultsView? If I copy your code, the following does update the ResultsBar when I click on it.
struct ContentView: View {
#ObservedObject var session = CurrentSession()
var body: some View {
ResultsView()
.environmentObject(session)
.onTapGesture {
self.session.generateGreenOrRed()
}
}
}
I had this same issue, but my issue was because I was updating the environment object on a background thread because I'm using an api. In order to fix my issue I just needed to wrapped setting my EnvironmentObject vars like the following.
DispatchQueue.main.async{
self.numberOfGreens = GetNumberOfGreens(id: id)
}
I hope this helps someone.