Activity indicator in SwiftUI

后端 未结 9 2025
忘了有多久
忘了有多久 2020-11-29 17:36

Trying to add a full screen activity indicator in SwiftUI.

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

With this, I c

相关标签:
9条回答
  • 2020-11-29 18:16

    iOS 14 - Native

    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 - 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 (                                                                    
    0 讨论(0)
  • 2020-11-29 18:17

    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:

    0 讨论(0)
  • 2020-11-29 18:17
    // 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)
        }
    }
    
    0 讨论(0)
  • 2020-11-29 18:18

    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)
     }
    }
    
    0 讨论(0)
  • 2020-11-29 18:19

    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


    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


    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


    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.

    0 讨论(0)
  • 2020-11-29 18:28

    If you want to a swift-ui-style solution, then this is the magic:

    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(!self.isAnimating ? 1 - CGFloat(index) / 5 : 0.2 + CGFloat(index) / 5)
                .offset(y: geometry.size.width / 10 - geometry.size.height / 2)
              }.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
        }
      }
    }
    

    Simply to use:

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

    Hope it helps!

    Example Usage:

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

    0 讨论(0)
提交回复
热议问题