SwiftUI-How to transition between text when a button is pressed - swift

I am making an exercise app and I want to transition between different exercises without making many seperate views as I am also making a timer which will publish too many times if I did that. I want to make it so that when I click a button, it will transition all the text from exercise1 to exercise 2 then to exercise 3 and so on. I want to transition all these using the same button.
So far what I have done is created navigation paths to different views that contain the different exercises but the timer broke when I did that because I called it many times causing it to countdown too fast. I want to see if I can contain all the exercises inside 1 view and transition between them. So for example I want to transition from:
Text(exercisePlan.exercise.title) to Text(exercisePlan.exercise2.title) then to Text(exercisePlan.exercise3.title) all with a click of one button
import SwiftUI
import AVKit
struct ExerciseScreenView: View {
#Binding var streaks: Streaks
#Binding var timerStruct: TimerStruct
var countdownTimer = 300
#State var player = AVPlayer()
var exercisePlan: ExercisePlan
#Binding var navigationPath: NavigationPath
func reset() {
timerStruct.countdownTimer = 300
timerStruct.timerRunning = false
}
var body: some View {
VStack {
Form {
Section(header: Text("1st Exercise (Scroll down for exercise steps)")) {
Text(exercisePlan.exercise.title)
.font(.system(size: 35, weight: .medium))
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
Section(header: Text("Video example")) {
VideoPlayer(player: exercisePlan.exercise.video)
.scaledToFit()
.frame(alignment: .center)
.cornerRadius(10)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
Section(header: Text("Steps:")){
Text(exercisePlan.exercise.steps)
.font(.system(size: 20, weight: .regular))
.padding()
.frame(alignment: .center)
}
}
TimerView(streaks: $streaks, timerStruct: $timerStruct)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 15, trailing: 0))
Button {
navigationPath.append("ExerciseScreen2View")
} label: {
Text("Next exercise")
.padding()
.background((Color(red: 184/255, green: 243/255, blue: 255/255)))
.foregroundColor(.black)
.cornerRadius(10)
.navigationBarBackButtonHidden()
}
.padding(EdgeInsets(top: 0, leading: 10, bottom: 10, trailing: 10))
}
}
}

What you want to do is create an array of the Exercises. Pressing the button should increase an index to get the relevant plan to show in your view.
Small code example:
struct ExerciseScreenView: View {
var exercises: [Exercise]
#State var index: 0
var body: some View {
VStack {
Text(exercises[index].title)
Button {
if exercisePlan.count - 1 > index {
index += 1
} else {
// leave screen
}
} label: {
Text("Next exercise")
}
}
}
}
}
This is a very basic example and can be improved by a lot, but it gives you a basic idea how to do it.

Related

Having issue with DispatchQueue not working after 3-4 executions in Swift

I have the following code below to show a Message to the user and then disappear. Everything works fine for the first 1 to 3 tries, but anything above that the Message window does not disappear as it did on the previous attempts/required.
struct ContentView: View {
#State var showingNotice = false
var body: some View {
ZStack {
Button(action: {
self.showingNotice = true
}, label: {
Text("Show Notice")
})
if showingNotice {
FloatingNotice(showingNotice: $showingNotice)
}
}
.animation(.easeInOut(duration: 1))
}
}
struct FloatingNotice: View {
#Binding var showingNotice: Bool
var body: some View {
VStack (alignment: .center, spacing: 8) {
Image(systemName: "checkmark")
.foregroundColor(.white)
.font(.system(size: 48, weight: .regular))
.padding(EdgeInsets(top: 20, leading: 5, bottom: 5, trailing: 5))
Text("Review added")
.foregroundColor(.white)
.font(.callout)
.padding(EdgeInsets(top: 0, leading: 10, bottom: 5, trailing: 10))
}
.background(Color.snackbar.opacity(0.75))
.cornerRadius(5)
.transition(.scale)
.onAppear(perform: {
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self.showingNotice = false
})
})
}
}
Can anyone see what I am missing or help figure why it "stops" working after multiple executions?
Sometimes onAppear will not called so your self.showingNotice = false is not getting called. So you can do one thing move your delay block inside the button click it self as below.
struct ContentView: View {
#State var showingNotice = false
var body: some View {
ZStack {
Button(action: {
self.showingNotice = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self.showingNotice = false
})
}, label: {
Text("Show Notice")
})
if showingNotice {
FloatingNotice(showingNotice: $showingNotice)
}
}
.animation(.easeInOut(duration: 1))
}
}
struct FloatingNotice: View {
#Binding var showingNotice: Bool
var body: some View {
VStack (alignment: .center, spacing: 8) {
Image(systemName: "checkmark")
.foregroundColor(.white)
.font(.system(size: 48, weight: .regular))
.padding(EdgeInsets(top: 20, leading: 5, bottom: 5, trailing: 5))
Text("Review added")
.foregroundColor(.white)
.font(.callout)
.padding(EdgeInsets(top: 0, leading: 10, bottom: 5, trailing: 10))
}
.background(Color(.red))
.cornerRadius(5)
.transition(.scale)
}
}

How to align with a certain view in a VStack?

I'm trying to achieve the following layout:
As you can see, Foo is aligned with Bar horizontally, and there's a vertical line at the bottom of Foo.
I haven't managed to do it, the best I can do is the following one:
Here is the corresponding code:
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
VStack(spacing: 5) {
Text("Foo")
.padding(.bottom, 0)
getVerticalLine()
}
Text("Bar")
.padding(10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.yellow)
}
}
private func getVerticalLine() -> some View {
return Color.gray
.frame(width: 1, height: 30)
.padding(.leading, 0)
}
}
My question is: how to modify the code to achieve the expected layout?
P.S. In the end, I want to achieve something like this:
Here is the complete code that you want. You need to use multiplier for constant so that it fits in all devices.
CententView
struct ContentView: View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
FooBarView(isLastOne: false)
.padding(EdgeInsets(top: 0, leading: 10, bottom: -18, trailing: 0))
FooBarView(isLastOne: false)
.padding(EdgeInsets(top: 0, leading: 10, bottom: -18, trailing: 0))
FooBarView(isLastOne: false)
.padding(EdgeInsets(top: 0, leading: 10, bottom: -18, trailing: 0))
FooBarView(isLastOne: true)
.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 0))
}
}
}
FooBarView
struct FooBarView: View {
var isLastOne: Bool
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
VStack(spacing: 5) {
Text("Foo")
.padding(.bottom, 0)
}
Text("Bar")
.padding(10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.yellow)
}
if !isLastOne {
getVerticalLine()
.padding(EdgeInsets(top: -5, leading: 12, bottom: 0, trailing: 0))
}
}
}
private func getVerticalLine() -> some View {
return Color.gray
.frame(width: 1, height: 40)
.padding(.leading, 0)
}
}
Each of your activities are a single HStack, with the time on the left. The line is just something that connects each row, not necessarily part of the row itself.
You can use a ZStack to show the gray line on the back and the rows on the front; a background on the first column will avoid overlapping the gray line.
The alignment is assured by giving the same frame width to the first column (time) and the vertical line.
Here's an example:
struct Example: View {
struct Activity: Hashable, Identifiable {
let id = UUID()
let time: String
let activity: String
}
let activities = [Activity(time: "10.00", activity: "Exercise"),
Activity(time: "11.00", activity: "Work"),
Activity(time: "12.00", activity: "Study"),
Activity(time: "13.00", activity: "Coding")]
let firstColumnWidth = 80.0
let spacingBetweenLines = 30.0
let internalPadding = 10.0
let lineHeight = 20.0
var body: some View {
ZStack {
HStack {
getVerticalLine
.frame(width: firstColumnWidth)
Spacer()
}
VStack(spacing: spacingBetweenLines) {
ForEach(activities) { item in
HStack(spacing: 5) {
Text(item.time)
.frame(height: lineHeight)
.padding(internalPadding)
.frame(width: firstColumnWidth)
.background(.white)
Text(item.activity)
.frame(height: lineHeight)
.padding(internalPadding)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.yellow)
}
}
}
}
.padding()
}
private var verticalLine: some View {
let lineHeight = internalPadding * 2 + lineHeight + spacingBetweenLines
return Color.gray
.frame(width: 1, height: lineHeight * Double(activities.count - 1))
.background(.green)
}
}

SwiftUI: Detect click on already selected list item

I have a List view with several items. All of then serve navigation purposes and all are Text. I cannot use NavigationLinks for this app.
That said, I am monitoring the list selection through a #State variable.
#State private var selection: String?
I print its value whever the view gets updated. It normally prints the unique id of every item when selecting different items of the list.
The problem is when trying to select the same item again: in this case the view does not get updated, and I don't see the debug message in my console.
I would like to be able to press on a list item to display a view, and then be able to press on it again, despite it already being selected because I have logic I would like to run (for example: manual view refresh).
How can I detect the re-selection of an already selected list item?
For any questions, please let me know. Thank you for your help!
Update 1
This is the code I am using:
Helper list item struct:
struct NavItem {
let id: String
let text: String
}
List:
struct NavList: View {
#Binding private var selection: String?
private var listItems: [NavItem]
//note:
//selection is a #State variable declared in a parent struct and passed here.
//This is optional. I could declare it as a #state too.
init(listItems: [NavItem], selection: Binding<String?>) {
self.listItems = listItems
self._selection = selection
debugPrint(self.selection)
}
var body: some View {
List (selection: $selection) {
ForEach(listItems, id:\.id) { item in
Text(item.text)
.tag(item.id)
}
}
}
}
Note 1: I already know this problem can be approached by using the .onTapGesture modifier. This solution seems to not be optimal. I tried seeking help for it here.
Note 2: In the project, the struct that contains the list conforms to View. When that view is displayed, it has the .listStyle(SidebarListStyle()) modifier added. This might be relevant.
there are a number of ways to do what you want,
try this with .onTapGesture{..}
List (selection: $selection) {
ForEach(listItems, id: \.id) { item in
Text(item.text)
.tag(item.id)
.padding(10)
.listRowInsets(.init(top: 0,leading: 0, bottom: 0, trailing: 0))
.frame(minWidth: 111, idealWidth: .infinity, maxWidth: .infinity, alignment: .leading)
.onTapGesture {
print("----> " + item.text)
}
}
}
or you can use a Button instead of onTapGesture;
List (selection: $selection) {
ForEach(listItems, id: \.id) { item in
Button(action: { print("----> " + item.text) }) {
Text(item.text)
}
.padding(10)
.listRowInsets(.init(top: 0,leading: 0, bottom: 0, trailing: 0))
.tag(item.id)
.frame(minWidth: 111, idealWidth: .infinity, maxWidth: .infinity, alignment: .leading)
}
}
EDIT: full test code:
This is the code I used in my tests:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
struct NavItem {
let id: String
let text: String
}
struct ContentView: View {
#State private var listItems: [NavItem] = [NavItem(id: "1", text: "1"),
NavItem(id: "2", text: "2"),
NavItem(id: "3", text: "3"),
NavItem(id: "4", text: "4")]
#State private var selection: String?
var body: some View {
List (selection: $selection) {
ForEach(listItems, id:\.id) { item in
Button(action: { print("----> " + item.text) }) {
Text(item.text)
}
.padding(10)
.listRowInsets(.init(top: 0,leading: -5, bottom: 0, trailing: -5))
.frame(minWidth: 111, idealWidth: .infinity, maxWidth: .infinity, alignment: .leading)
.tag(item.id)
.background(Color.red) // <--- last modifier
}
}
}
}

SwiftUI - Remove space between cells

I am implementing a list view in SwiftUI. What I am trying to approach is to have cells that has no space between any other cells or parent view.
So in this screenshot as you can see there is a space between every cell and also space with the edge of the phone, which I want to remove.
struct FlickrView : View {
var flickrResponse: [FlickrResponse]
var body: some View {
List(flickrResponse) { item in
FlickrImageCell(response: item)
}
}
}
struct FlickrImageCell : View {
var response: FlickrResponse
var body: some View {
return ZStack(alignment: .topLeading) {
Image(uiImage: response.image ?? UIImage())
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: nil, height: 100.0, alignment: .center)
.clipShape(Rectangle())
.padding(0)
Text(response.title).fontWeight(.medium).multilineTextAlignment(.center)
}
}
}
I have tried this modifier:
.padding(EdgeInsets(top: 0, leading: -20, bottom: 20, trailing: -20))
But I have two problems with this approach: First, I don't think its convenient to write literal negative values. Second, the bottom padding does not work with any value.
So any suggestions?
I've had good luck with listRowInsets
struct ContentView: View {
var body: some View {
List {
Color.red
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
Color.blue
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
Color.yellow
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}
}

Custom view won't use state variable update provided through binding, but debug watch shows changes

I'm starting to understand the #Binding and #State ways of the SwiftUI. Or at least I like to think so. Anyway, there is some debugging results that puzzle me a lot. Let me explain.
The objective here is to control the position of a "floating" view on the ContentView. This involves a message sending a #binding variable to a #State in the ContentView. This works: you can see it in the debugger. The expected result is a floating rectangle that changes position in the screen when the gear button is pushed.
The floating view can be passed its own #State to control 'where' it floats (high or low in the 'y' coordinate). This works if the ViewPosition is passed hardcoded.
Now, the problem is that the puzzle works well if you watch the values being passed in the debugger, but the fact is that the floatong view always uses the same value to work with. How can this be?
We can see the effects in the attached code, setting breakpoints in the lines 120 and 133 if the alternative case is watched or 76 if the default case is being watched.
Code is cut-paste in a new project for a tabbed swiftui app.
I tried both coarse ways that are presented for two different ContentView options (rename to change the execution branch). It is important to watch the variables in the debugger in order to enjoy the full puzzlement experience as .high and .low values are being well passed, but the rectangle remains still.
//
// ContentView.swift
// TestAppUno
//
import SwiftUI
struct MenuButton1: View {
#Binding var menuButtonAction: Bool
var title: String = "--"
var body: some View {
Button(action: {self.menuButtonAction.toggle()}) {
Image(systemName:"gear")
.resizable()
.imageScale(.large)
.aspectRatio(1, contentMode: .fit)
.frame(minWidth: 50, maxWidth: 50, minHeight: 50, maxHeight: 50, alignment: .topLeading)
}
.background(Color.white.opacity(0))
.cornerRadius(5)
.padding(.vertical, 10)
.position(x: 30, y: 95)
}
}
struct MenuButton2: View {
#Binding var menuButtonAction: ViewPosition
var title: String = "--"
var body: some View {
Button(action: {self.toggler()}) {
Image(systemName:"gear")
.resizable()
.imageScale(.large)
.aspectRatio(1, contentMode: .fit)
.frame(minWidth: 50, maxWidth: 50, minHeight: 50, maxHeight: 50, alignment: .topLeading)
}
.background(Color.white.opacity(0))
.cornerRadius(5)
//.border(Color.black, width: 1)
.padding(.vertical, 10)
.position(x: 30, y: 95)
}
func toggler()->ViewPosition {
if (self.menuButtonAction == ViewPosition.high) { self.menuButtonAction = ViewPosition.low; return ViewPosition.low } else { self.menuButtonAction = ViewPosition.high; return ViewPosition.low }
}
}
struct ContentView: View {
#State private var selection = 0
#State var moveCard = false
#State var vpos = ViewPosition.low
var body: some View {
TabbedView(selection: $selection){
ZStack() {
MenuButton2(menuButtonAction: $vpos)
//if(self.moveCard){self.vpos = ViewPosition.low} else {self.vpos = ViewPosition.low }
// Correct answer, change 1 of 3
//TestView(aposition: $vpos) { // <-- OK
TestView(aposition:self.vpos) {
VStack(alignment: HorizontalAlignment.center, spacing: 1.0){
Text("See here")
.font(.headline)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
}
}
.tabItemLabel(Image("first"))
.tag(0)
Text("Nothing here")
.tabItemLabel(Image("second"))
.tag(1)
}
}
}
// Correct answer, change 2 of 3
struct ContentView1: View { // <-- Remove this block
#State private var selection = 0
#State var moveCard = false
#State var cardpos = ViewPosition.low
var body: some View {
TabbedView(selection: $selection){
ZStack() {
MenuButton1(menuButtonAction: $moveCard)
if(self.moveCard){
TestView(aposition:ViewPosition.low) {
VStack(alignment: HorizontalAlignment.center, spacing: 1.0){
Text("See here")
.font(.headline)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
}
}else{
TestView(aposition:ViewPosition.high) {
VStack(alignment: HorizontalAlignment.center, spacing: 1.0){
Text("See here")
.font(.headline)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
}
}
}
.tabItemLabel(Image("first"))
.tag(0)
Text("Nothing here")
.tabItemLabel(Image("second"))
.tag(1)
}
}
}
struct TestView<Content: View> : View {
#State var aposition : ViewPosition
//proposed solution #1
//var aposition : ViewPosition
//proposed solution #2 -> Correct
//#Binding var aposition : ViewPosition // <- Correct answer, change 3 of 3
var content: () -> Content
var body: some View {
print("Position: " + String( format: "%.3f", Double(self.aposition.rawValue)))
return Group {
self.content()
}
.frame(height: UIScreen.main.bounds.height/2)
.frame(width: UIScreen.main.bounds.width)
.background(Color.red)
.cornerRadius(10.0)
.shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0)
.offset(y: self.aposition.rawValue )
}
}
enum ViewPosition: CGFloat {
case high = 50
case low = 500
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
No errors, compile well and variables are passed. Hardcoded passed values to the floating view can be made, and the rectangle responds, but not if values are provided programmatically.
Just remove the #State in #State var aposition in the TestView. Basically, #State variables are meant to represent sources of truth, so they are never passed a value from a higher View.
I could write a long explanation on how bindings work, but it is perfectly explained in the WWDC session Data Flow with SwiftUI. It only takes 37 minutes and will save you a lot of time eventually. It makes the clear distinction of when you need to use: #State, #Binding, #BindingObject and #EnvironmentObject.
struct TestView<Content: View> : View {
var aposition : ViewPosition
var content: () -> Content
var body: some View {
print("Position: " + String( format: "%.3f", Double(self.aposition.rawValue)))
return Group {
self.content()
}
.frame(height: UIScreen.main.bounds.height/2)
.frame(width: UIScreen.main.bounds.width)
.background(Color.red)
.cornerRadius(10.0)
.shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0)
.offset(y: self.aposition.rawValue )
}
}