292

I'm trying to add a full screen activity indicator in SwiftUI.

I can use .overlay(overlay: ) function in a View Protocol.

With this, I can make any view overlay, but I can't find the iOS default style UIActivityIndicatorView equivalent in SwiftUI.

How can I make a default style spinner with SwiftUI?

NOTE: This is not about adding an activity indicator in UIKit framework.

1
  • Make sure to file a feedback issue with Apple using the Feedback Assistant. Getting requests in early during the beta process is the best way to see what you want in the framework. Commented Jun 11, 2019 at 20:47

18 Answers 18

488

As of Xcode 12 beta (iOS 14), a new view called ProgressView is available to developers, and that can display both determinate and indeterminate progress.

Its style defaults to CircularProgressViewStyle, which is exactly what we're looking for.

var body: some View {
    VStack {
        ProgressView()
           // and if you want to be explicit / future-proof...
           // .progressViewStyle(CircularProgressViewStyle())
    }
}

Xcode 11.x

Quite a few views are not yet represented in SwiftUI, but it's easily to port them into the system. You need to wrap UIActivityIndicator and make it UIViewRepresentable.

(More about this can be found in the excellent WWDC 2019 talk - Integrating SwiftUI)

struct ActivityIndicator: UIViewRepresentable {

    @Binding var isAnimating: Bool
    let style: UIActivityIndicatorView.Style

    func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
        return UIActivityIndicatorView(style: style)
    }

    func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) {
        isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
    }
}

Then you can use it as follows - here's an example of a loading overlay.

Note: I prefer using ZStack, rather than overlay(:_), so I know exactly what's going on in my implementation.

struct LoadingView<Content>: View where Content: View {

    @Binding var isShowing: Bool
    var content: () -> Content

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .center) {

                self.content()
                    .disabled(self.isShowing)
                    .blur(radius: self.isShowing ? 3 : 0)

                VStack {
                    Text("Loading...")
                    ActivityIndicator(isAnimating: .constant(true), style: .large)
                }
                .frame(width: geometry.size.width / 2,
                       height: geometry.size.height / 5)
                .background(Color.secondary.colorInvert())
                .foregroundColor(Color.primary)
                .cornerRadius(20)
                .opacity(self.isShowing ? 1 : 0)

            }
        }
    }

}

To test it, you can use this example code:

struct ContentView: View {

    var body: some View {
        LoadingView(isShowing: .constant(true)) {
            NavigationView {
                List(["1", "2", "3", "4", "5"], id: \.self) { row in
                    Text(row)
                }.navigationBarTitle(Text("A List"), displayMode: .large)
            }
        }
    }

}

Result:

enter image description here

Sign up to request clarification or add additional context in comments.

15 Comments

But how to stop it?
@Bagusflyer you can interact with the isAnimating binding inside ActivityIndicator
@Alfi in his code it says isShowing: .constant(true). That means the indicator is always showing. What you need to do is have an @State variable that is true when you want the loading indicator to appear (when the data is loading), and then change that to false when you want the loading indicator to disappear (when the data is done loading). If the variable is called isDataLoading for example, you would do isShowing: $isDataLoading instead of where Matteo put isShowing: .constant(true).
@MatteoPacini you do not actually need a Binding for this as it is not being modified inside ActivityIndicator or in LoadingView. Just a regular boolean variable works. Binding is useful for when you want to modify the variable inside the view and pass that change back out to the parent.
@nelsonPARRILLA I suspect that tintColor only works on pure Swift UI views - not on bridged (UIViewRepresentable) ones.
|
151

iOS 14 and above

it's just a simple view.

ProgressView()

Currently, it's defaulted to CircularProgressViewStyle but you can manually set the style of it by adding the following modifer:

.progressViewStyle(CircularProgressViewStyle())

Also, the style could be anything that conforms to ProgressViewStyle


iOS 13 and above

Fully customizable Standard UIActivityIndicator in SwiftUI: (Exactly as a native View):

You can build and configure it (as much as you could in the original UIKit):

ActivityIndicator(isAnimating: loading)
    .configure { $0.color = .yellow } // Optional configurations (🎁 bouns)
    .background(Color.blue)

Result


Just implement this base struct and you will be good to go:

struct ActivityIndicator: UIViewRepresentable {
    
    typealias UIView = UIActivityIndicatorView
    var isAnimating: Bool
    fileprivate var configuration = { (indicator: UIView) in }

    func makeUIView(context: UIViewRepresentableContext<Self>) -> UIView { UIView() }
    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<Self>) {
        isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
        configuration(uiView)
    }
}

🎁 Bouns Extension:

With this little helpful extension, you can access the configuration through a modifier like other SwiftUI views:

extension View where Self == ActivityIndicator {
    func configure(_ configuration: @escaping (Self.UIView)->Void) -> Self {
        Self.init(isAnimating: self.isAnimating, configuration: configuration)
    }
}

The classic way:

Also you can configure the view in a classic initializer:

ActivityIndicator(isAnimating: loading) { 
    $0.color = .red
    $0.hidesWhenStopped = false
    //Any other UIActivityIndicatorView property you like
}

This method is fully adaptable. For example, you can see How to make TextField become the first responder with the same method here

4 Comments

How to change color of ProgressView?
.progressViewStyle(CircularProgressViewStyle(tint: Color.red)) will change the color
Your "Bonus Extension: configure()" calls init for the second time, taking up memory. Am I right? or is it so highly optimized that we are allowed to do such a chain invocation of init?
It’s a sugar, this is not very expensive for this case but i didn’t measure the performance hit for big views. You can measure and change the implementation to something more efficient (since it is a class) but initializing a struct is not that much expensive to be worried about
111

If you want a SwiftUI-style solution, then this is the magic:

import Foundation
import SwiftUI

struct ActivityIndicator: View {
    
    @State private var isAnimating: Bool = false
    
    var body: some View {
        GeometryReader { (geometry: GeometryProxy) in
            ForEach(0..<5) { index in
                Circle()
                    .frame(
                        width: geometry.size.width / 5, 
                        height: geometry.size.height / 5
                    )
                    .scaleEffect(calcScale(index: index))
                    .offset(y: calcYOffset(geometry))
                    .frame(
                        width: geometry.size.width, 
                        height: geometry.size.height
                    )
                    .rotationEffect(
                        !self.isAnimating ? .degrees(0) : .degrees(360)
                    )
                    .animation(
                        Animation
                            .timingCurve(
                                0.5, 0.15 + Double(index) / 5, 0.25,
                                1, duration: 1.5
                            )
                            .repeatForever(autoreverses: false)
                    )
            }
        }
        .aspectRatio(1, contentMode: .fit)
        .onAppear {
            self.isAnimating = true
        }
    }
    
    func calcScale(index: Int) -> CGFloat {
        (!isAnimating ? 1 - CGFloat(index) / 5 : 0.2 + CGFloat(index) / 5)
    }
    
    func calcYOffset(_ geometry: GeometryProxy) -> CGFloat {
        geometry.size.width / 10 - geometry.size.height / 2
    }
}

Hope it helps!

Example Usage:

ActivityIndicator()
    .frame(width: 200, height: 200)
    .foregroundColor(.orange)

ActivityIndicator GIF

7 Comments

This helped me so much, thanks a lot! You can define functions to create the Circles and add view modifier for the animations to make it more readable.
how would i remove animation if the isAnimating is a State , may a @Binding instead?
Error In newest Xcode and Swift: "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
That looks great. Only problem is that the animaiton broken when I present it modally using a .fullScreenCover and it's inside a NavigationView... Then all the dots fly in from the top left of the screen...
A great solution. Now there is an apple deprecation, though. Solved with: .animation( Animation .timingCurve(0.5, 0.15 + Double(index) / 5, 0.25, 1, duration: 1.5) .repeatForever(autoreverses: false), value: isAnimating)
|
36

Custom Indicators

Although Apple supports native Activity Indicator now from the SwiftUI 2.0, You can Simply implement your own animations. These are all supported on SwiftUI 1.0. Also it is working in widgets.

Arcs

struct Arcs: View {
    @Binding var isAnimating: Bool
    let count: UInt
    let width: CGFloat
    let spacing: CGFloat

    var body: some View {
        GeometryReader { geometry in
            ForEach(0..<Int(count)) { index in
                item(forIndex: index, in: geometry.size)
                    .rotationEffect(isAnimating ? .degrees(360) : .degrees(0))
                    .animation(
                        Animation.default
                            .speed(Double.random(in: 0.2...0.5))
                            .repeatCount(isAnimating ? .max : 1, autoreverses: false)
                    )
            }
        }
        .aspectRatio(contentMode: .fit)
    }

    private func item(forIndex index: Int, in geometrySize: CGSize) -> some View {
        Group { () -> Path in
            var p = Path()
            p.addArc(center: CGPoint(x: geometrySize.width/2, y: geometrySize.height/2),
                     radius: geometrySize.width/2 - width/2 - CGFloat(index) * (width + spacing),
                     startAngle: .degrees(0),
                     endAngle: .degrees(Double(Int.random(in: 120...300))),
                     clockwise: true)
            return p.strokedPath(.init(lineWidth: width))
        }
        .frame(width: geometrySize.width, height: geometrySize.height)
    }
}

Demo of different variations Arcs


Bars

struct Bars: View {
    @Binding var isAnimating: Bool
    let count: UInt
    let spacing: CGFloat
    let cornerRadius: CGFloat
    let scaleRange: ClosedRange<Double>
    let opacityRange: ClosedRange<Double>

    var body: some View {
        GeometryReader { geometry in
            ForEach(0..<Int(count)) { index in
                item(forIndex: index, in: geometry.size)
            }
        }
        .aspectRatio(contentMode: .fit)
    }

    private var scale: CGFloat { CGFloat(isAnimating ? scaleRange.lowerBound : scaleRange.upperBound) }
    private var opacity: Double { isAnimating ? opacityRange.lowerBound : opacityRange.upperBound }

    private func size(count: UInt, geometry: CGSize) -> CGFloat {
        (geometry.width/CGFloat(count)) - (spacing-2)
    }

    private func item(forIndex index: Int, in geometrySize: CGSize) -> some View {
        RoundedRectangle(cornerRadius: cornerRadius,  style: .continuous)
            .frame(width: size(count: count, geometry: geometrySize), height: geometrySize.height)
            .scaleEffect(x: 1, y: scale, anchor: .center)
            .opacity(opacity)
            .animation(
                Animation
                    .default
                    .repeatCount(isAnimating ? .max : 1, autoreverses: true)
                    .delay(Double(index) / Double(count) / 2)
            )
            .offset(x: CGFloat(index) * (size(count: count, geometry: geometrySize) + spacing))
    }
}

Demo of different variations Bars


Blinkers

struct Blinking: View {
    @Binding var isAnimating: Bool
    let count: UInt
    let size: CGFloat

    var body: some View {
        GeometryReader { geometry in
            ForEach(0..<Int(count)) { index in
                item(forIndex: index, in: geometry.size)
                    .frame(width: geometry.size.width, height: geometry.size.height)

            }
        }
        .aspectRatio(contentMode: .fit)
    }

    private func item(forIndex index: Int, in geometrySize: CGSize) -> some View {
        let angle = 2 * CGFloat.pi / CGFloat(count) * CGFloat(index)
        let x = (geometrySize.width/2 - size/2) * cos(angle)
        let y = (geometrySize.height/2 - size/2) * sin(angle)
        return Circle()
            .frame(width: size, height: size)
            .scaleEffect(isAnimating ? 0.5 : 1)
            .opacity(isAnimating ? 0.25 : 1)
            .animation(
                Animation
                    .default
                    .repeatCount(isAnimating ? .max : 1, autoreverses: true)
                    .delay(Double(index) / Double(count) / 2)
            )
            .offset(x: x, y: y)
    }
}

Demo of different variations Blinkers


For the sake of preventing walls of code, you can find more elegant indicators in this repo hosted on the git.

Note that all these animations have a Binding that MUST toggle to be run.

8 Comments

This is great! I've found one bug though - there is a really strange animation for iActivityIndicator(style: .rotatingShapes(count: 10, size: 15))
what is the issue with the iActivityIndicator().style(.rotatingShapes(count: 10, size: 15)) by the way? @pawello2222 ?
If you set the count to 5 or less, the animation looks fine (looks similar to this answer). However, if you set the count to 15, the leading dot doesn't stop at the top of the circle. It starts doing another cycle, then comes back to the top and then starts the cycle again. I'm not sure if it's intended. Tested on simulator only, Xcode 12.0.1.
Hmmmm. That's because animations are not serialized. I should add a serializing option to the framework for that. Thanks for sharing your opinion.
@MojtabaHosseini how do you toggle the binding to run?
|
14
struct ContentView: View {
    
    @State private var isCircleRotating = true
    @State private var animateStart = false
    @State private var animateEnd = true
    
    var body: some View {
        
        ZStack {
            Circle()
                .stroke(lineWidth: 10)
                .fill(Color.init(red: 0.96, green: 0.96, blue: 0.96))
                .frame(width: 150, height: 150)
            
            Circle()
                .trim(from: animateStart ? 1/3 : 1/9, to: animateEnd ? 2/5 : 1)
                .stroke(lineWidth: 10)
                .rotationEffect(.degrees(isCircleRotating ? 360 : 0))
                .frame(width: 150, height: 150)
                .foregroundColor(Color.blue)
                .onAppear() {
                    withAnimation(Animation
                                    .linear(duration: 1)
                                    .repeatForever(autoreverses: false)) {
                        self.isCircleRotating.toggle()
                    }
                    withAnimation(Animation
                                    .linear(duration: 1)
                                    .delay(0.5)
                                    .repeatForever(autoreverses: true)) {
                        self.animateStart.toggle()
                    }
                    withAnimation(Animation
                                    .linear(duration: 1)
                                    .delay(1)
                                    .repeatForever(autoreverses: true)) {
                        self.animateEnd.toggle()
                    }
                }
        }
    }
}

enter image description here

Comments

7

Activity Indicator or Loader

enter image description here

struct LoaderView: View {
    
    @State var isAnimating = false
    
    var body: some View {
        ZStack {
            Circle()
                .trim(from: 0, to: 0.7)
                .stroke(Color("Blue"), lineWidth: 4)
                .frame(width: 80, height: 80)
                .rotationEffect(.degrees(isAnimating ? 360 : 0))
                .foregroundColor(Color.blue)
                .onAppear() {
                    withAnimation(Animation
                        .linear(duration: 1)
                        .repeatForever(autoreverses: false)) {
                            self.isAnimating = true
                        }
                }
        }
    }
}

Usage:

struct HomeView: View {
    
    @State var isShowLoader : Bool = false
    
    var body: some View {
        ZStack {
            Button(action: {
                isShowLoader.toggle()
            }) {
                Text("Loader")
            }
            .buttonStyle(.plain)
            
            if isShowLoader {
                LoaderView()
            }
        }
    }
}

SwiftUI Activity indicator for macOS and iOS.

Comments

6

Using symbolEffect

You can create many effects with a little effort like these: Demo

For example, here is the code to recreate the default iOS progress indicator using symbols:

Image(systemName: "progress.indicator")
    .symbolEffect(
        .variableColor.iterative,
        options: .repeat(.continuous),
        value: isLoading
)

Note some of the effect and symbols are only available from specific platform version

1 Comment

progress.indicator doesn't seem to be an SF Symbol that exists...??
5

Activity indicator in SwiftUI


import SwiftUI

struct Indicator: View {

    @State var animateTrimPath = false
    @State var rotaeInfinity = false

    var body: some View {

        ZStack {
            Color.black
                .edgesIgnoringSafeArea(.all)
            ZStack {
                Path { path in
                    path.addLines([
                        .init(x: 2, y: 1),
                        .init(x: 1, y: 0),
                        .init(x: 0, y: 1),
                        .init(x: 1, y: 2),
                        .init(x: 3, y: 0),
                        .init(x: 4, y: 1),
                        .init(x: 3, y: 2),
                        .init(x: 2, y: 1)
                    ])
                }
                .trim(from: animateTrimPath ? 1/0.99 : 0, to: animateTrimPath ? 1/0.99 : 1)
                .scale(50, anchor: .topLeading)
                .stroke(Color.yellow, lineWidth: 20)
                .offset(x: 110, y: 350)
                .animation(Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true))
                .onAppear() {
                    self.animateTrimPath.toggle()
                }
            }
            .rotationEffect(.degrees(rotaeInfinity ? 0 : -360))
            .scaleEffect(0.3, anchor: .center)
            .animation(Animation.easeInOut(duration: 1.5)
            .repeatForever(autoreverses: false))
            .onAppear(){
                self.rotaeInfinity.toggle()
            }
        }
    }
}

struct Indicator_Previews: PreviewProvider {
    static var previews: some View {
        Indicator()
    }
}

Activity indicator in SwiftUI

Comments

5

A convenient way in SwiftUI that I found useful is 2 step approach:

  1. Create a ViewModifier that will embed your view into ZStack and add progress indicator on top. Could be something like this:

     struct LoadingIndicator: ViewModifier {
     let width = UIScreen.main.bounds.width * 0.3
     let height =  UIScreen.main.bounds.width * 0.3
    
     func body(content: Content) -> some View {
         return ZStack {
             content
                 .disabled(true)
                 .blur(radius: 2)
    
             //gray background
             VStack{}
                 .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
                 .background(Color.gray.opacity(0.2))
                 .cornerRadius(20)
                 .edgesIgnoringSafeArea(.all)
    
             //progress indicator
             ProgressView()
                 .frame(width: width, height: height)
                 .background(Color.white)
                 .cornerRadius(20)
                 .opacity(1)
                 .shadow(color: Color.gray.opacity(0.5), radius: 4.0, x: 1.0, y: 2.0)
           }
    }
    
  2. Create view extension that will make conditional modifier application available to any view:

     extension View {
     /// Applies the given transform if the given condition evaluates to `true`.
     /// - Parameters:
     ///   - condition: The condition to evaluate.
     ///   - transform: The transform to apply to the source `View`.
     /// - Returns: Either the original `View` or the modified `View` if the condition is `true`.
     @ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
         if condition {
             transform(self)
         } else {
             self
         }
       }
    }
    
  3. Usage is very intuitive. Suppose that myView() returns whatever your view is. You just conditionally apply the modifier using .if view extension from step 2:

     var body: some View {
         myView()
           .if(myViewModel.isLoading){ view in
             view.modifier(LoadingIndicator())
         }
     }
    

In case that myViewModel.isLoading is false, no modifier will be applied, so loading indicator won't show.

Of course, you can use any kind of progress indicator you wish - default or your own custom one.

Comments

4

I implemented the classic UIKit indicator using SwiftUI. See the activity indicator in action here

struct ActivityIndicator: View {
  @State private var currentIndex: Int = 0

  func incrementIndex() {
    currentIndex += 1
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50), execute: {
      self.incrementIndex()
    })
  }

  var body: some View {
    GeometryReader { (geometry: GeometryProxy) in
      ForEach(0..<12) { index in
        Group {
          Rectangle()
            .cornerRadius(geometry.size.width / 5)
            .frame(width: geometry.size.width / 8, height: geometry.size.height / 3)
            .offset(y: geometry.size.width / 2.25)
            .rotationEffect(.degrees(Double(-360 * index / 12)))
            .opacity(self.setOpacity(for: index))
        }.frame(width: geometry.size.width, height: geometry.size.height)
      }
    }
    .aspectRatio(1, contentMode: .fit)
    .onAppear {
      self.incrementIndex()
    }
  }

  func setOpacity(for index: Int) -> Double {
    let opacityOffset = Double((index + currentIndex - 1) % 11 ) / 12 * 0.9
    return 0.1 + opacityOffset
  }
}

struct ActivityIndicator_Previews: PreviewProvider {
  static var previews: some View {
    ActivityIndicator()
      .frame(width: 50, height: 50)
      .foregroundColor(.blue)
  }
}

Comments

3

In addition to Mojatba Hosseini's answer, I've made a few updates so that this can be put in a swift package:

Activity indicator:

import Foundation
import SwiftUI
import UIKit

public struct ActivityIndicator: UIViewRepresentable {

  public typealias UIView = UIActivityIndicatorView
  public var isAnimating: Bool = true
  public var configuration = { (indicator: UIView) in }

 public init(isAnimating: Bool, configuration: ((UIView) -> Void)? = nil) {
    self.isAnimating = isAnimating
    if let configuration = configuration {
        self.configuration = configuration
    }
 }

 public func makeUIView(context: UIViewRepresentableContext<Self>) -> UIView {
    UIView()
 }

 public func updateUIView(_ uiView: UIView, context: 
    UIViewRepresentableContext<Self>) {
     isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
     configuration(uiView)
}}

Extension:

public extension View where Self == ActivityIndicator {
func configure(_ configuration: @escaping (Self.UIView) -> Void) -> Self {
    Self.init(isAnimating: self.isAnimating, configuration: configuration)
 }
}

1 Comment

how can i use this?
2

It's really easy with SwiftUI 2.0 I made this simple and easy custom view with ProgressView

Here is how it looks:

enter image description here

Code:

import SwiftUI

struct ActivityIndicatorView: View {
    @Binding var isPresented:Bool
    var body: some View {
        if isPresented{
            ZStack{
                RoundedRectangle(cornerRadius: 15).fill(CustomColor.gray.opacity(0.1))
                ProgressView {
                    Text("Loading...")
                        .font(.title2)
                }
            }.frame(width: 120, height: 120, alignment: .center)
            .background(RoundedRectangle(cornerRadius: 25).stroke(CustomColor.gray,lineWidth: 2))
        }
    }
}

Comments

1

Try this:

import SwiftUI

struct LoadingPlaceholder: View {
    var text = "Loading..."
    init(text:String ) {
        self.text = text
    }
    var body: some View {
        VStack(content: {
            ProgressView(self.text)
        })
    }
}

More information about at SwiftUI ProgressView

Comments

1

my 2 cents for nice and simpler code of batuhankrbb, showing use of isPresented in timer... or other stuff... (I will use it in url callback..)

//
//  ContentView.swift
//
//  Created by ing.conti on 27/01/21.


import SwiftUI

struct ActivityIndicatorView: View {
    @Binding var isPresented:Bool
    var body: some View {
        if isPresented{
            ZStack{
                RoundedRectangle(cornerRadius: 15).fill(Color.gray.opacity(0.1))
                ProgressView {
                    Text("Loading...")
                        .font(.title2)
                }
            }.frame(width: 120, height: 120, alignment: .center)
            .background(RoundedRectangle(cornerRadius: 25).stroke(Color.gray,lineWidth: 2))
        }
    }
}



struct ContentView: View {
    @State var isPresented = false
    @State var counter = 0
    var body: some View {
        
        VStack{
            Text("Hello, world! \(counter)")
                .padding()
            
            ActivityIndicatorView(isPresented: $isPresented)
        }.onAppear(perform: {
            _ = startRefreshing()
        })
    }
    
    
    
    func startRefreshing()->Timer{
        
        let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
            
            counter+=1
            print(counter)
            if counter>2{
                isPresented = true
            }
            
            if counter>4{
                isPresented = false
                timer.invalidate()
            }
        }
        return timer
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Comments

1

I have modified Matteo Pacini's answer for macOS using AppKit and SwiftUI. This allows you to use NSProgressIndicator in SwiftUI while retaining capability for macOS 10.15.

import AppKit
import SwiftUI

struct ActivityIndicator: NSViewRepresentable {
    
    @Binding var isAnimating: Bool
    let style: NSProgressIndicator.Style

    func makeNSView(context: NSViewRepresentableContext<ActivityIndicator>) -> NSProgressIndicator {
        let progressIndicator = NSProgressIndicator()
        progressIndicator.style = self.style
        return progressIndicator
    }

    func updateNSView(_ nsView: NSProgressIndicator, context: NSViewRepresentableContext<ActivityIndicator>) {
        isAnimating ? nsView.startAnimation(nil) : nsView.stopAnimation(nil)
    }
    
}

Usage is as follows:

ActivityIndicator(isAnimating: .constant(true), style: .spinning)

Comments

0
// Activity View

struct ActivityIndicator: UIViewRepresentable {

    let style: UIActivityIndicatorView.Style
    @Binding var animate: Bool

    private let spinner: UIActivityIndicatorView = {
        $0.hidesWhenStopped = true
        return $0
    }(UIActivityIndicatorView(style: .medium))

    func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
        spinner.style = style
        return spinner
    }

    func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) {
        animate ? uiView.startAnimating() : uiView.stopAnimating()
    }

    func configure(_ indicator: (UIActivityIndicatorView) -> Void) -> some View {
        indicator(spinner)
        return self
    }   
}

// Usage
struct ContentView: View {

    @State var animate = false

    var body: some View {
            ActivityIndicator(style: .large, animate: $animate)
                .configure {
                    $0.color = .red
            }
            .background(Color.blue)
    }
}

Comments

0

UIActivityIndicator equivalent in SwiftUI is ProgressView(). In addition you can change progress view color with tint property.

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            ProgressView()
                .scaleEffect(2) /// Bigger
                .tint(.red) /// Change color
        }
    }
}

#Preview {
    ContentView()
}

Example

You can find more information about what kind of progress view adapts to your needs. SwiftUI ProgressView

1 Comment

Thank you for your interest in contributing to the Stack Overflow community. This question already has quite a few answers—including one that has been extensively validated by the community. Are you certain your approach hasn’t been given previously? If so, it would be useful to explain how your approach is different, under what circumstances your approach might be preferred, and/or why you think the previous answers aren’t sufficient. Can you kindly edit your answer to offer an explanation?
0

extract progress loader logic using modifiers. Simply call the addProgressAndLoader(...) from your Views. It's good to create a BaseViewModel and store @Published showAlert for making it common. You can simply pass your Binding as well instead of ViewModel

        import SwiftUI
    
        public extension View {
        
        @ViewBuilder
        func addProgressAndLoader(viewModel: ViewModel) -> some View {
            let alertBinding = Binding<Bool>(
                get: { viewModel.showAlert == true },
                        set: { _ in viewModel.showAlert = false }
                    )
            
            self.modifier(ProgressViewModifier(isLoading: viewModel.isLoading))
        }
    }
        
  struct ProgressViewModifier: ViewModifier {
            @EnvironmentObject var theme: ThemeManager
        
            var isLoading: Bool
            let isEnabled: Bool
            
            init(isLoading: Bool, isEnabled: Bool = true) {
                self.isLoading = isLoading
                self.isEnabled = isEnabled
            }
            
            func body(content: Content) -> some View {
                ZStack(alignment: .center) {
                    content
                        .disabled(isLoading)
                    if isLoading && isEnabled {
                        ProgressView()
                            .progressViewStyle(CircularProgressViewStyle(tint: theme.currentTheme.onSurfaceColor))
                    }
                }
            }
        }

    enter code here

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.