240

Trying to add a full screen activity indicator in SwiftUI.

I can use .overlay(overlay: ) function in 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 activity indicator in UIKit framework.

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
Johnykutty
  • 12,091
  • 13
  • 59
  • 100
  • I tried to find it also, and failed, guess it will be added later :) – Markicevic Jun 10 '19 at 15:38
  • 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. – Jon Shier Jun 11 '19 at 20:47
  • You can find a [Fully customizable Native Standard version here](https://stackoverflow.com/a/59056440/5623035) – Mojtaba Hosseini Nov 26 '19 at 18:16

14 Answers14

435

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

Matteo Pacini
  • 21,796
  • 7
  • 67
  • 74
  • 4
    But how to stop it? – Bagusflyer Aug 28 '19 at 08:46
  • @Bagusflyer you can interact with the `isAnimating` binding inside `ActivityIndicator` – Matteo Pacini Sep 05 '19 at 23:12
  • 1
    Hi @MatteoPacini, Thanks for your answer. But, can you please help me how can i hide the activity indicator. Can you please put down the code for this? – Programming Learner Nov 22 '19 at 12:09
  • This worked almost great, but when I tap the button, in this case to perform a network operation the button gets stuck in tapped state, everything else works great, the network operation is being executed in a separate thread – Pedro Cavaleiro Dec 05 '19 at 04:42
  • @Annu see https://peacemoon.de/blog/2019/06/10/activity-indicator-with-swiftui/ – jpulikkottil Jan 02 '20 at 12:52
  • @Matteo Pacini please give us more detail of stopping this thing! how to bind it? bind it with what? how? – Ahmadreza Jan 18 '20 at 06:58
  • 7
    @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)`. – RPatel99 Feb 15 '20 at 00:52
  • There are other ways to do it too (like making a custom binding that checks other conditions), but the scenario I mentioned above is the easiest to explain and most likely to occur. – RPatel99 Feb 15 '20 at 00:53
  • 4
    @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. – Helam Mar 03 '20 at 22:44
  • Do you know why tintColor has no effect with this implementation of ActivityIndicator ? When I do ActivityIndicator(isAnimating: .constant(true), style: .large).tintColor(.red), it stay black. – nelson PARRILLA Mar 19 '20 at 11:51
  • 1
    @nelsonPARRILLA I suspect that `tintColor` only works on pure Swift UI views - not on bridged (`UIViewRepresentable`) ones. – Matteo Pacini May 04 '20 at 01:51
  • How can we put a bool in the new `ProgressView`? smth like ProgressView($showing) { Content() } – M1X Jun 23 '20 at 14:01
  • `UIViewRepresentable` is. not working for **widget**s – Mojtaba Hosseini Oct 16 '20 at 21:24
  • Great post! In the `ActivityIndicator` you can remove the `return` statement for `makeUIView` so that it uses implicit return for both functions, for code symmetry. I tried to do an edit, but was rejected :( – Sebbo Dec 30 '20 at 17:41
  • @MatteoPacini Hi, how do you pass a dummy value for the content in the preview `LoadingOverlayView(isShowing: .constant(true), content: <#() -> _#>)`? – Isuru Mar 02 '21 at 12:18
  • @MatteoPacini @nelsonPARRILLA if you adapt `makeUIView` as follows you can color the spinner `let indicator = UIActivityIndicatorView(style: style)` `indicator.color = .orange` `return indicator` – beks6 Jan 23 '22 at 23:02
  • You can set @State isShowing in your view where you are using and toggle when you finished loading. – Shashi3456643 Jul 14 '23 at 19:04
131

iOS 14

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

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • How to change color of ProgressView? – Bagusflyer Nov 05 '20 at 05:47
  • ```.progressViewStyle(CircularProgressViewStyle(tint: Color.red))``` will change the color – Bagusflyer Nov 05 '20 at 05:55
  • 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? – Ruzard Nov 26 '20 at 12:33
  • 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 – Mojtaba Hosseini Nov 26 '20 at 12:42
100

If you want to a swift-ui-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
                Group {
                    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 {
        return (!isAnimating ? 1 - CGFloat(Float(index)) / 5 : 0.2 + CGFloat(index) / 5)
    }
    
    func calcYOffset(_ geometry: GeometryProxy) -> CGFloat {
        return geometry.size.width / 10 - geometry.size.height / 2
    }
    
}

Simply to use:

ActivityIndicator()
.frame(width: 50, height: 50)

Hope it helps!

Example Usage:

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

enter image description here

Twitter khuong291
  • 11,328
  • 15
  • 80
  • 116
KitKit
  • 8,549
  • 12
  • 56
  • 82
  • 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. – Arif Ata Cengiz Dec 16 '19 at 10:40
  • 2
    Love this solution! – Jon Vogel May 03 '20 at 04:42
  • 1
    how would i remove animation if the isAnimating is a State , may a @Binding instead? – Di Nerd Apps May 30 '20 at 22:14
  • 4
    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" – Keeano Jun 11 '21 at 17:01
  • How to add background view? – Manish Jan 28 '22 at 09:28
  • 1
    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... – Georg May 06 '22 at 11:18
  • there is a strange visual effect: instead of the circle animating in the middle of the screen, it starts in the upper-left side of the screen, then animates to the center, back and forth. any solution for this. – tp2376 May 09 '22 at 05:02
  • 1
    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)``` – Faisal Memon Feb 02 '23 at 20:06
  • @Georg I have the same problem with NavigationView. Did you find a solution? – isabsent Jul 06 '23 at 08:21
32

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.

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • This is great! I've found one bug though - there is a really strange animation for `iActivityIndicator(style: .rotatingShapes(count: 10, size: 15))` – pawello2222 Oct 14 '20 at 13:32
  • what is the issue with the `iActivityIndicator().style(.rotatingShapes(count: 10, size: 15))` by the way? @pawello2222 ? – Mojtaba Hosseini Oct 16 '20 at 17:02
  • If you set the `count` to 5 or less, the animation looks fine (looks similar to [this answer](https://stackoverflow.com/a/59171234/8697793)). 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. – pawello2222 Oct 16 '20 at 17:11
  • Hmmmm. That's because animations are not serialized. I should add a serializing option to the framework for that. Thanks for sharing your opinion. – Mojtaba Hosseini Oct 16 '20 at 17:14
  • @MojtabaHosseini how do you toggle the binding to run? – GarySabo Oct 21 '20 at 17:06
  • Just assign it to a bool and a button. There is an issue with SwiftUI animation so you need to wrap it inside another view if you need an autoplay version. I explained in a demo at the repo. @GarySabo – Mojtaba Hosseini Oct 21 '20 at 19:37
  • there is a strange visual effect: instead of the circle animating in the middle of the screen, it starts in the upper-left side of the screen, then animates to the center, back and forth. any solution for this. @m – tp2376 May 09 '22 at 05:02
  • Seems like at least the Bars could use some updates to recent syntax. – Jonny Aug 13 '22 at 02:02
11
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

Arvind Patel
  • 441
  • 6
  • 13
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

Rashid Latif
  • 2,809
  • 22
  • 26
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)
  }
}

Yisselda
  • 71
  • 4
4

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.

Despotovic
  • 1,807
  • 2
  • 20
  • 24
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)
 }
}
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
moyoteg
  • 351
  • 3
  • 11
3

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))
        }
    }
}
Isuru
  • 30,617
  • 60
  • 187
  • 303
batuhankrbb
  • 598
  • 7
  • 10
2

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)
Brad
  • 21
  • 3
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

Pedro Trujillo
  • 1,559
  • 18
  • 19
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()
    }
}
ingconti
  • 10,876
  • 3
  • 61
  • 48
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)
    }
}
ChrisMM
  • 8,448
  • 13
  • 29
  • 48
Manish
  • 29
  • 3