I created this custom progress view with bars for the progress, how can I animate the the green color in the next bar when it's time to go to the next element? I am looking for it to fill the bar slowly from left to right
struct ContentView: View {
var body: some View {
ProgressBar(lastIndex: 8, currentIndex: 4)
.padding(.horizontal, 23)
.padding(.top, 10)
Spacer()
}
}
struct ProgressBar: View {
var lastIndex: Int
var currentIndex: Int
var body: some View {
HStack(alignment: .center, spacing: 4) {
ForEach(0...lastIndex, id: \.self) { i in
BarElement(selected: i <= currentIndex)
}
}
}
}
struct BarElement: View {
var selected: Bool
var body: some View {
Capsule()
.fill(selected ? .green : Color(UIColor.lightGray))
.frame(width: 33, height: 5, alignment: .center)
}
}
It's hard to answer without code but if you have all separated rectangles, it should be enough using .animation(.linear) on all the rectagles, so when the values changes it should animated as you asked.
This will do what you wanted.
struct BarElement: View {
var value : CGFloat = 0.0
var selected: Bool
var body: some View {
ZStack (alignment: .leading){
Capsule()
.fill(Color(UIColor.lightGray))
.frame(width: 33, height: 5, alignment: .leading)
.animation(.linear, value: selected)
Capsule()
.fill(.green)
.frame(width: selected ? 33 : 0, height: 5, alignment: .leading)
.animation(.linear, value: selected)
}
}
}
Related
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)
}
}
I created a custom LoadingView as a Indicator for loading objects from internet. When add it to NavigationView, it shows like this
enter image description here
I only want it showing in the middle of screen rather than move from top left corner
Here is my Code
struct LoadingView: View {
#State private var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.primaryDota, lineWidth: 5)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.onAppear {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.isLoading.toggle()
}
}
}
}
and my content view
struct ContentView: View {
var body: some View {
NavigationView {
LoadingView()
.frame(width: 30, height: 30)
}
}
}
This looks like a bug of NavigationView: without it animation works totally fine. And it wan't fixed in iOS15.
Working solution is waiting one layout cycle using DispatchQueue.main.async before string animation:
struct LoadingView: View {
#State private var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.red, lineWidth: 5)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.onAppear {
DispatchQueue.main.async {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.isLoading.toggle()
}
}
}
}
}
This is a bug from NavigationView, I tried to kill all possible animation but NavigationView ignored all my try, NavigationView add an internal animation to children! here all we can do right now!
struct ContentView: View {
var body: some View {
NavigationView {
LoadingView()
}
}
}
struct LoadingView: View {
#State private var isLoading: Bool = Bool()
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.blue, lineWidth: 5.0)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false), value: isLoading)
.onAppear { DispatchQueue.main.async { isLoading.toggle() } }
}
}
I am having problems with making a simple systemIcon flash in SwiftUI.
I got the animation working, but it has a silly behaviour if the layout of
a LazyGridView changes or adapts. Below is a video of its erroneous behaviour.
The flashing bell stays in place but when the layout rearranges the bell
starts transitioning in from the bottom of the parent view thats not there anymore.
Has someone got a suggestion how to get around this?
Here is a working example which is similar to my problem
import SwiftUI
struct FlashingBellLazyVGrid: View {
#State var isAnimating = false
#State var showChart = true
var body: some View {
let columns = [GridItem(.adaptive(minimum: 300), spacing: 50, alignment: .center)]
VStack {
Button(action: {
showChart.toggle()
}) {
VStack {
Circle()
.fill(showChart ? Color.green : Color.red)
.shadow(color: Color.gray, radius: 5, x: 2, y: 2)
Text("Charts")
.foregroundColor(Color.primary)
}.frame(width: 150, height: 50)
}
ScrollView {
LazyVGrid (
columns: columns, spacing: 50
) {
ForEach(0 ..< 25) { item in
ZStack {
Rectangle()
.fill(Color.red)
.cornerRadius(15)
VStack {
HStack {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
Spacer()
Image(systemName: "bell.fill")
.foregroundColor(Color.yellow)
.opacity(self.isAnimating ? 1 : 0)
.animation(Animation.easeInOut(duration: 0.66).repeatForever(autoreverses: false))
.onAppear{ self.isAnimating = true }
}.padding(50)
if showChart {
Rectangle()
.fill(Color.green)
.frame(height: 200)
}
}
}
}
}
}
}
}
}
struct FlashingBellLazyVGrid_Previews: PreviewProvider {
static var previews: some View {
FlashingBellLazyVGrid()
}
}
how it looks like before you click the showChart button at the top
After you toggle the button it looks like the bells are erroneously moving into place from the bottom of the screen. and toggling it back to its original state doesn't resolve this bug subsequently.
[
Looks like the animation is basing itself off of the original size of the view. In order to trick it into recognizing the new view size, I used .id(UUID()) on the outside of the grid. In a real world application, you'd probably want to be careful to store this ID somewhere and only refresh it when needed -- not on every re-render like I'm doing:
struct FlashingBellLazyVGrid: View {
#State var showChart = true
let columns = [GridItem(.adaptive(minimum: 300), spacing: 50, alignment: .center)]
var body: some View {
VStack {
Button(action: {
showChart.toggle()
}) {
VStack {
Circle()
.fill(showChart ? Color.green : Color.red)
.shadow(color: Color.gray, radius: 5, x: 2, y: 2)
Text("Charts")
.foregroundColor(Color.primary)
}.frame(width: 150, height: 50)
}
ScrollView {
LazyVGrid (
columns: columns, spacing: 50
) {
ForEach(0 ..< 25) { item in
ZStack {
Rectangle()
.fill(Color.red)
.cornerRadius(15)
VStack {
SeparateComponent()
if showChart {
Rectangle()
.fill(Color.green)
.frame(height: 200)
}
}
}
}
}
.id(UUID()) //<-- Here
}
}
}
}
struct SeparateComponent : View {
#State var isAnimating : Bool = false
var body: some View {
HStack {
Text("Hello, World!")
Spacer()
Image(systemName: "bell.fill")
.foregroundColor(Color.yellow)
.opacity(self.isAnimating ? 1 : 0)
.animation(Animation.easeInOut(duration: 0.66).repeatForever(autoreverses: false))
.onAppear{
self.isAnimating = true
}
}
.padding(50)
}
}
I also separated out the blinking component into its own view, since there were already problematic things happening with the existing logic with onAppear, which wouldn't affect newly-scrolled-to items correctly. This may need refactoring for your particular case as well, but this should get you started.
I have a tricky design. I'm trying to have a close button that from whatever views, inside a "fake" Modal View (fake cause it's full screen, thanks to a code that i found online), it's gonna close the Modal View.
Right now the close button is working in the first view that open in the modal view, but I need it to work also in the next views of the modal view, cause I have a flow inside this modal view to create an item.
This is the View from which I start ColorNewItemView, my fake Modal View.
struct RecapItemToAdd: View {
#State var isPresented: Bool = false
var body: some View {
NavigationView {
VStack {
Button(action: { withAnimation { self.isPresented.toggle()}}) {
Image(systemName: "pencil")
.resizable()
.renderingMode(.original)
.frame(width: 13, height: 17)
.foregroundColor(UIManager.hLightGrey)
}
}
ZStack {
VStack(alignment: .leading) {
ColorNewItemView(isPresenteded: self.$isPresented)
Spacer()
}
}
.background(Color.white)
.edgesIgnoringSafeArea(.all)
.offset(x: 0, y: self.isPresented ? 0 : UIApplication.shared.keyWindow?.frame.height ?? 0)
}
}
}
Note: I know that "keyWindow" is deprecated but I don't know how to change it.
With ColorNewItemView starts my full screen Modal View. In this view the close button works.
struct ColorNewItemView: View {
#State var selection: Int? = nil
#Binding var isPresenteded: Bool
var body: some View {
NavigationStackView {
VStack(alignment: .center) {
Button(action: {
self.isPresenteded = false
}) {
Image(systemName: "xmark.circle.fill")
.resizable()
.frame(width: 30, height: 30)
.foregroundColor(UIManager.hBlueLight)
}
Text("First View")
.font(UIManager.einaTitle)
.foregroundColor(UIManager.hDarkBlue)
Image("black-hoodie")
.resizable()
.renderingMode(.original)
.frame(width: 245, height: 300)
PushView(destination: Color2NewItemView(isPresenteded: self.$isPresenteded), tag: 1, selection: $selection) {
Button(action: {self.selection = 1}) {
Text("Avanti")
.font(UIManager.einaButton)
.foregroundColor(.white)
.frame(width: 291, height: 43)
.background(UIManager.buttonGradient)
.cornerRadius(6)
.shadow(color: UIManager.hBlueShadow, radius: 7, x: 0.0, y: 6.0)
}
}
}
}
}
}
Now I have the next view inside the Modal view, where the close button starts to stop working.
struct Color2NewItemView: View {
#Binding var isPresenteded: Bool
#State var selection: Int? = nil
var body: some View {
VStack(alignment: .center) {
Button(action: {
self.isPresenteded = false
}) {
Image(systemName: "xmark.circle.fill")
.resizable()
.frame(width: 30, height: 30)
.foregroundColor(UIManager.hBlueLight)
}
Text("Second View")
.font(UIManager.einaTitle)
.foregroundColor(UIManager.hDarkBlue)
Image("black-hoodie")
.resizable()
.renderingMode(.original)
.frame(width: 245, height: 300)
PushView(destination: FabricNewItemView(isPresenteded: $isPresenteded), tag: 1, selection: $selection) {
Button(action: {self.selection = 1}) {
Text("Tessuto")
.font(UIManager.einaButton)
.foregroundColor(.white)
.frame(width: 291, height: 43)
.background(UIManager.buttonGradient)
.cornerRadius(6)
.shadow(color: UIManager.hBlueShadow, radius: 7, x: 0.0, y: 6.0)
}
}
Spacer()
.frame(height: 18)
PopView{
Text("Back")
.font(UIManager.einaBodySemibold)
.foregroundColor(UIManager.hGrey)
}
}
}
}
Ps.
I had also to use a library called NavigationStack, since I have a custom back button on the bottom of the page, and the Navigation View doesn't let me pop back without using the back in the navigation bar.
Binding can be lost on deep view hierarchy, so it is more appropriate to operate with it on the level it was received.
Here is possible approach using EnvironmentKey (by same idea as presentationMode works)
Introduce helper environment key which holds some closure
struct DismissModalKey: EnvironmentKey {
typealias Value = () -> ()
static let defaultValue = { }
}
extension EnvironmentValues {
var dismissModal: DismissModalKey.Value {
get {
return self[DismissModalKey.self]
}
set {
self[DismissModalKey.self] = newValue
}
}
}
so in your top modal view you can inject into hierarchy callback to dismiss
struct ColorNewItemView: View {
#State var selection: Int? = nil
#Binding var isPresented: Bool
var body: some View {
NavigationStackView {
// ... other code
}
.environment(\.dismissModal, { self.isPresented = false} ) // << here !!
}
}
thus this environment value now is available for all subviews, and you can use it as
struct Color2NewItemView: View {
#Environment(\.dismissModal) var dismissModal
#State var selection: Int? = nil
var body: some View {
VStack(alignment: .center) {
Button(action: {
self.dismissModal() // << here !!
}) {
// ... other code
}
}
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 )
}
}