Rectangle progress bar swiftUI - swift

Hey does someone know how can I create rectangle progress bar in swiftUI?
Something like this?
https://i.stack.imgur.com/CMwB3.gif
I have tried this:
struct ProgressBar: View
{
#State var degress = 0.0
#Binding var shouldLoad: Bool
var body: some View
{
RoundedRectangle(cornerRadius: cornerRadiusValue)
.trim(from: 0.0, to: CGFloat(degress))
.stroke(Color.Scheme.main, lineWidth: 2.0)
.frame(width: 300, height: 40, alignment: .center)
.onAppear(perform: shouldLoad == true ? {self.start()} : {})
}
func start()
{
Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true)
{
timer in
withAnimation
{
self.degress += 0.3
}
}
}
}

Here is simple demo of possible approach for [0..1] range progress indicator.
Updated: re-tested with Xcode 13.3 / iOS 15.4
struct ProgressBar: View {
#Binding var progress: CGFloat // [0..1]
var body: some View {
RoundedRectangle(cornerRadius: 10)
.trim(from: 0.0, to: CGFloat(progress))
.stroke(Color.red, lineWidth: 2.0)
.animation(.linear, value: progress)
}
}
struct DemoAnimatingProgress: View {
#State private var progress = CGFloat.zero
var body: some View {
Button("Demo") {
if self.progress == .zero {
self.simulateLoading()
} else {
self.progress = 0
}
}
.padding()
.background(ProgressBar(progress: $progress))
}
func simulateLoading() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.progress += 0.1
if self.progress < 1.0 {
self.simulateLoading()
}
}
}
}

Available for XCode 12.
import SwiftUI
//MARK: - ProgressBar
struct ContentView: View {
#State private var downloaded = 0.0
var body: some View {
ProgressView("Downloaded...", value: downloaded, total: 100)
}
}
//MARK: - Circular ProgressBar
struct ContentView: View {
#State private var downloaded = 0.0
var body: some View {
ProgressView("Downloaded...", value: downloaded, total: 100)
.progressViewStyle(CircularProgressViewStyle())
}
}

Related

I can't change on to the next screen when progress bar animation is over

#EnvironmentObject var viewRouter: ViewRouter
#State var percent: CGFloat = 0
var body: some View {
VStack {
ProgressBar(width:200, height: 30, percent: percent)
.onAppear(perform: {
withAnimation(Animation.easeIn.speed(0.1)) {
while percent < 100 {
percent += 1
}
viewRouter.currentPage = .homepage
}
})
}
}
Animation doesn't come to an end but immediately change the View to .homepage.
Another edit: try something like this test code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
//#EnvironmentObject var viewRouter: ViewRouter
#State var percent: CGFloat = 0
#State var showHomePage = false
func doProgress() {
if percent < 100 {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
percent += 1
doProgress()
}
} else {
showHomePage.toggle()
// viewRouter.currentPage = .homepage
}
}
var body: some View {
NavigationView {
VStack {
ProgressView("", value: percent, total: 100)
.onAppear {
doProgress()
}
NavigationLink(
destination: Text("the next screen view"),
isActive: $showHomePage,
label: { EmptyView() } )
}
}
}
}

ProgressBar iOS 13

I've tried creating my own ProgressView to support iOS 13, but for some reason it appears to not work. I've tried #State, #Binding and the plain var progress: Progress, but it doesn't update at all.
struct ProgressBar: View {
#Binding var progress: Progress
var body: some View {
VStack(alignment: .leading) {
Text("\(Int(progress.fractionCompleted))% completed")
ZStack {
RoundedRectangle(cornerRadius: 2)
.foregroundColor(Color(UIColor.systemGray5))
.frame(height: 4)
GeometryReader { metrics in
RoundedRectangle(cornerRadius: 2)
.foregroundColor(.blue)
.frame(width: metrics.size.width * CGFloat(progress.fractionCompleted))
}
}.frame(height: 4)
Text("\(progress.completedUnitCount) of \(progress.totalUnitCount)")
.font(.footnote)
.foregroundColor(.gray)
}
}
}
In my content view I added both the iOS 14 variant and the iOS 13 supporting one. They look the same, but the iOS 13 variant does not change anything.
struct ContentView: View {
#State private var progress = Progress(totalUnitCount: 10)
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
ProgressBar(progress: $progress)
.padding()
.onReceive(timer) { timer in
progress.completedUnitCount += 1
if progress.isFinished {
self.timer.upstream.connect().cancel()
progress.totalUnitCount = 500
}
}
if #available(iOS 14, *) {
ProgressView(progress)
.padding()
.onReceive(timer) { timer in
progress.completedUnitCount += 1
if progress.isFinished {
self.timer.upstream.connect().cancel()
progress.totalUnitCount = 500
}
}
}
}
}
}
The iOS 14 variant works, but my iOS 13 implementation fails. Can somebody help me?
Progress is-a NSObject, it is not a struct, so state does not work for it. You have to use KVO to observe changes in Progress and redirect into SwiftUI view's source of truth.
Here is a simplified demo of possible solution. Tested with Xcode 12.1 / iOS 14.1
class ProgressBarViewModel: ObservableObject {
#Published var fractionCompleted: Double
let progress: Progress
private var observer: NSKeyValueObservation!
init(_ progress: Progress) {
self.progress = progress
self.fractionCompleted = progress.fractionCompleted
observer = progress.observe(\.completedUnitCount) { [weak self] (sender, _) in
self?.fractionCompleted = sender.fractionCompleted
}
}
}
struct ProgressBar: View {
#ObservedObject private var vm: ProgressBarViewModel
init(_ progress: Progress) {
self.vm = ProgressBarViewModel(progress)
}
var body: some View {
VStack(alignment: .leading) {
Text("\(Int(vm.fractionCompleted * 100))% completed")
ZStack {
RoundedRectangle(cornerRadius: 2)
.foregroundColor(Color(UIColor.systemGray5))
.frame(height: 4)
GeometryReader { metrics in
RoundedRectangle(cornerRadius: 2)
.foregroundColor(.blue)
.frame(width: metrics.size.width * CGFloat(vm.fractionCompleted))
}
}.frame(height: 4)
Text("\(vm.progress.completedUnitCount) of \(vm.progress.totalUnitCount)")
.font(.footnote)
.foregroundColor(.gray)
}
}
}
and updated call place to use same constructor
struct ContentView: View {
#State private var progress = Progress(totalUnitCount: 10)
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
ProgressBar(progress) // << here !!
// ... other code

Disable animation when a view appears in SwiftUI

I got a problem while trying to display a custom loading view in SwiftUI.
I created a custom struct view OrangeActivityIndicator:
struct OrangeActivityIndicator: View {
var style = StrokeStyle(lineWidth: 6, lineCap: .round)
#State var animate = false
let orangeColor = Color.orOrangeColor
let orangeColorOpaque = Color.orOrangeColor.opacity(0.5)
init(lineWidth: CGFloat = 6) {
style.lineWidth = lineWidth
}
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [orangeColor, orangeColorOpaque]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false))
}.onAppear() {
self.animate.toggle()
}
}
}
I use it inside different screens or views, my problem is that it appears weirdly, for example in CampaignsView of the app I display it when the server call is in progress.
struct CampaignsView: View {
#ObservedObject var viewModel: CampaignsViewModel
var body: some View {
NavigationView {
ZStack {
VStack(spacing: 0) {
CustomNavigationBar(campaignsNumber: viewModel.cardCampaigns.count)
.padding([.leading, .trailing], 24)
.frame(height: 25)
CarouselView(x: $viewModel.x, screen: viewModel.screen, op: $viewModel.op, count: $viewModel.index, cardCampaigns: $viewModel.cardCampaigns).frame(height: 240)
CampaignDescriptionView(idx: viewModel.index, cardCampaigns: viewModel.cardCampaigns)
.padding([.leading, .trailing], 24)
Spacer()
}
.onAppear {
self.viewModel.getCombineCampaigns()
}
if viewModel.isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
}
}
.padding(.top, 34)
.background(Color.orBackgroundGrayColor.edgesIgnoringSafeArea(.all))
.navigationBarHidden(true)
}
}
}
The Indicator itself is correctly spinning, the problem is when it appears, it appears as a translation animation coming from the bottom to the middle of the screen. This is my viewModel with the server call and isLoading property:
class CampaignsViewModel: ObservableObject {
#Published var index: Int = 0
#Published var cardCampaigns: [CardCampaign] = [CardCampaign]()
#Published var isLoading: Bool = false
var cancellable: AnyCancellable?
func getCombineCampaigns() {
self.isLoading = true
let campaignLoader = CampaignLoader()
cancellable = campaignLoader.getCampaigns()
.receive(on: DispatchQueue.main)
//Handle Events operator is used for debugging.
.handleEvents(receiveSubscription: { print("Receive subscription: \($0)") },
receiveOutput: { print("Receive output: \($0)") },
receiveCompletion: { print("Receive completion: \($0)") },
receiveCancel: { print("Receive cancel") },
receiveRequest: { print("Receive request: \($0)") })
.sink { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { campaignResult in
self.isLoading = false
guard let campaignsList = campaignResult.content else {
return
}
self.cardCampaigns = campaignsList.map { campaign in
return CardCampaign(campaign: campaign)
}
self.moveToFirstCard()
}
}
}
Use animation with linked related state
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [orangeColor, orangeColorOpaque]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.animation(Animation.linear(duration: 0.7)
.repeatForever(autoreverses: false),
value: animate) // << here !!
}.onAppear() {
self.animate.toggle()
}
}
SwiftUI 3
(iOS 15)
use the new API
.animation(.default, value: isAppeared)
SwiftUI 1, 2
Like below, when I first load a view, the button was animated.
So, I used the GeometryReader.
The animation is set to nil while the coordinates of the dummy are changing.
RecordView.swift
struct RecordView: View {
var body: some View {
Button(action: { }) {
Color(.red)
.frame(width: 38, height: 38)
.cornerRadius(6)
.padding(34)
.overlay(Circle().stroke(Color(.red), lineWidth: 8))
}
.buttonStyle(ButtonStyle1()) // 👈 customize the button.
}
}
ButtonStyle.swift
import SwiftUI
struct ButtonStyle1: ButtonStyle {
// In my case, the #State isn't worked, so I used class.
#ObservedObject private var data = ButtonStyle1Data()
func makeBody(configuration: Self.Configuration) -> some View {
ZStack {
// Dummy for tracking the view status
GeometryReader {
Color.clear
.preference(key: FramePreferenceKey.self, value: $0.frame(in: .global))
}
.frame(width: 0, height: 0)
.onPreferenceChange(FramePreferenceKey.self) { frame in
guard !data.isAppeared else { return }
// ⬇️ This is the key
data.isLoaded = 0 <= frame.origin.x
}
// Content View
configuration.label
.opacity(configuration.isPressed ? 0.5 : 1)
.scaleEffect(configuration.isPressed ? 0.92 : 1)
.animation(data.isAppeared ? .easeInOut(duration: 0.18) : nil)
}
}
}
ButtonStyleData.swift
final class ButtonStyle1Data: ObservableObject {
#Published var isAppeared = false
}
FramePreferenceKey.swift
struct FramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {}
}
Result

Trigger a animation from view outside

I want to make an animation view, it will rotate when clicked.
like this:
struct AnimatedView: View {
#State var degree: Double = 0.0
var body: some View {
ZStack {
Image("picture")
.rotationEffect(.degrees(degree))
.animation(
Animation.linear(duration: 2)
)
Button(action: {
self.degree = 180.0
}) {
Text("animate").frame(width: 100, height: 100)
}
}
}
func active() {
self.degree = 180.0
}
}
I place this view in ContentView, and I want to active the animation from other button not in the view.
I found function active can't change degree value, so the animation not start.
I knew use #Blinding keep degree variable in ContentView may work, but I don't like too much variable in there.
As you said, you need to use a #Binding, i.e.
struct ContentView: View {
#State private var degree: Double = 0.0
var body: some View {
VStack {
AnimatedView(degree: $degree)
Button(action: {
self.degree += 180.0
}) {
Text("animate")
}
}
}
}
struct AnimatedView: View {
#Binding var degree: Double
var body: some View {
Image(systemName: "checkmark")
.rotationEffect(.degrees(degree))
.animation(
Animation.linear(duration: 2)
)
}
}

Array of structs not updating in a view

I have an array of observed object, that contains an array of structs, that contain data. I would like to show it onscreen. This data is originally shown onscreen but the changes are not pushed whenever I make a change to an item in the array. I am even changing the property within the struct. I have tried it in my manager class aswell. I've done a bit of digging, changing my approach several times, but I can't get this working. I am very new to swiftui/swift as-well-as stack-overflow.
Full code:
struct GameView: View {
#State var value: CGFloat = 0
#ObservedObject var circles = GameCircles()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Color.primary
.frame(width: 450, height: 800)
self.circles.getViews()
.onReceive(timer) { _ in
self.circles.tick()
}
}
.edgesIgnoringSafeArea(.all)
.gesture(DragGesture(minimumDistance: 20)
.onChanged { gest in
self.value = gest.location.x
})
}
}
class GameCircles: ObservableObject {
#Published var circles: [GameCircle] = []
func getViews() -> some View {
ForEach(circles, id: \.id) { circle in
circle.makeView()
}
}
func tick() {
for circle in circles {
circle.tick()
print(circle.y)
}
circles.append(GameCircle(x: Int.random(in: -200...200), y: -200))
}
}
struct GameCircle: Identifiable {
#State var x: Int
#State var y: Int
let color = Color.random()
var id = UUID()
func tick() {
self.y += 1
}
func makeView() -> some View {
return ZStack {
Circle()
.frame(width: 40, height: 40)
.foregroundColor(color)
.animation(.default)
Text("\(Int(y))")
.foregroundColor(.black)
}
.offset(x: CGFloat(self.x), y: CGFloat(self.y))
}
}
I played with this code around, trying to solve your problem. And it was surprise for me, that state var in View didn't change in ForEach loop (you'll see it at the screenshot). Ok, I rewrite your code, now circles going down:
// MARK: models
class GameCircles: ObservableObject {
#Published var circles: [CircleModel] = []
func addNewCircle() {
circles.append(CircleModel(x: CGFloat.random(in: -200...200), y: -200))
}
}
struct CircleModel: Identifiable, Equatable {
let id = UUID()
var x: CGFloat
var y: CGFloat
mutating func pushDown() {
self.y += 5
}
}
// MARK: views
struct GameView: View {
#State var gameSeconds = 0
#ObservedObject var game = GameCircles()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Color.primary
.frame(width: 450, height: 800)
ForEach(self.game.circles) { circle in
CircleView(y: circle.y)
.offset(x: circle.x, y: circle.y)
.onReceive(self.timer) { _ in
let circleIndex = self.game.circles.firstIndex(of: circle)!
self.game.circles[circleIndex].pushDown()
}
}
.onReceive(self.timer) { _ in
self.game.addNewCircle()
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct CircleView: View {
#State var y: CGFloat
var body: some View {
ZStack {
Circle()
.frame(width: 40, height: 40)
.foregroundColor(.red)
.animation(.default)
Text("\(Int(y))")
.foregroundColor(.black)
}
}
}
struct GameView_Previews: PreviewProvider {
static var previews: some View {
GameView()
}
}
as you can see #State var y doesn't change and I wonder why? Nevertheless, I hope it could help you.
P.S. I rewrote the code for a few times, so that is not the only solution and you may use tick() func as in the question and the code will be more clear
class GameCircles: ObservableObject {
#Published var circles: [CircleModel] = []
func tick() {
for index in circles.indices {
circles[index].pushDown()
}
circles.append(CircleModel(x: CGFloat.random(in: -200...200), y: -200))
}
}
struct GameView: View {
#State var gameSeconds = 0
#ObservedObject var game = GameCircles()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Color.primary
.frame(width: 450, height: 800)
ForEach(self.game.circles) { circle in
CircleView(y: circle.y)
.offset(x: circle.x, y: circle.y)
}
.onReceive(self.timer) { _ in
self.game.tick()
}
}
.edgesIgnoringSafeArea(.all)
}
}