How to use same set of modifiers for various shapes - swiftui

As a part of my learning SwiftUI project I do some shape rotations and I have code below. I'm wondering how to avoid of same three lines of modifiers for each shape.
func getShape(shape: Int, i: Int) -> AnyView {
switch shape {
case 0:
return AnyView(Rectangle()
.stroke(colors[Int(shapeColor)])
.frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
.rotationEffect(Angle(degrees: Double(i) * Double(angleStep))))
case 1:
return AnyView(Capsule()
.stroke(colors[Int(shapeColor)])
.frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
.rotationEffect(Angle(degrees: Double(i) * Double(angleStep))))
case 2:
return AnyView(Ellipse()
.stroke(colors[Int(shapeColor)])
.frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
.rotationEffect(Angle(degrees: Double(i) * Double(angleStep))))
default:
return AnyView(Rectangle()
.stroke(colors[Int(shapeColor)])
.frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
.rotationEffect(Angle(degrees: Double(i) * Double(angleStep))))
}
}

Using the helper AnyShape type eraser
struct AnyShape: Shape {
private let builder: (CGRect) -> Path
init<S: Shape>(_ shape: S) {
builder = { rect in
let path = shape.path(in: rect)
return path
}
}
func path(in rect: CGRect) -> Path {
return builder(rect)
}
}
your function can be written as
func getShape(shape: Int, i: Int) -> some View {
let selectedShape: AnyShape = {
switch shape {
case 0:
return AnyShape(Rectangle())
case 1:
return AnyShape(Capsule())
case 2:
return AnyShape(Ellipse())
default:
return AnyShape(Rectangle())
}
}()
return selectedShape
.stroke(colors[Int(shapeColor)])
.frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
.rotationEffect(Angle(degrees: Double(i) * Double(angleStep))))
}

You can abstract some of the repetition away by using helper functions and extensions.
In the simplified example below I use a #ViewBuilder to clean up the code that we are returning. There is no need to use AnyView and it makes the code much easier to read.
It would be great if we could return some Shape however this is
not currently possible and results in errors. This is why the stroke
value has to be repeated for each Shape in the getShape
function, otherwise we could have made the extension on Shape
instead of View.
I create an extension on View that allows us to combine the modifiers into one function making it more readable and easier to use. Though honestly this part is optional and you could use your two modifiers frame and rotationEffect.
The #ViewBuilder getShape(shape:index:) returns the the shape you have picked with its chosen color, this is then used by the function createShape(shape:index:) where you add the custom modifier that we created as an extension on View.
Finally we create our shape
This should give you a starting point.
struct ShapeView: View {
#ViewBuilder // 1
func getShape(shape: Int, i: Int) -> some View {
switch shape {
case 0:
Rectangle().stroke(Color.red)
case 1:
Capsule().stroke(Color.red)
case 2:
Ellipse().stroke(Color.red)
default:
Rectangle().stroke(Color.red)
}
}
func createShape(shape: Int, index: Int) -> some View { // 3
getShape(shape: shape, i: index)
.myModifier(width: 200, height: 100, index: index, angleStep: 30)
}
var body: some View {
createShape(shape: 2, index: 1) // 4
}
}
// 2
extension View {
func myModifier(width: CGFloat, height: CGFloat, index: Int, angleStep: Double) -> some View {
self
.frame(width: width, height: height)
.rotationEffect(Angle(degrees: Double(index) * Double(angleStep)))
}
}
struct ShapeView_Previews: PreviewProvider {
static var previews: some View {
ShapeView()
}
}
It is such a shame that we cannot return some Shape from the #ViewBuilder or if there existed a #ShapeBuilder as this would mean that we wouldn't have to add the stroke to each shape individually, as a View cannot have a stroke.

A nested function can help cleaning up the code:
func getShape(shape: Int, i: Int) -> some View {
func adjustedView<S: Shape>(shape: S) -> some View {
shape
.stroke(colors[Int(shapeColor)])
.frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
.rotationEffect(Angle(degrees: Double(i) * Double(angleStep))))
}
return Group {
switch shape {
case 0:
adjustedView(shape: Rectangle())
case 1:
adjustedView(shape: Capsule())
case 2:
adjustedView(shape: Ellipse())
default:
adjustedView(shape: Rectangle())
}
}
}
Another option is to extend Shape with a convenience function. I.e.
extension Shape {
func adjust(shapeWidth: Double, shapeHeight: Double, angle: Angle) -> some View {
self.stroke()
//.stroke(colors[Int(shapeColor)]) // for brevity
.frame(width: CGFloat(shapeWidth), height: CGFloat(shapeHeight))
.rotationEffect(angle)
}
}
It simplifies code a bit. Also there is no need to erase types.
func getShape(shape: Int, i: Int) -> some View {
Group {
switch shape {
case 0:
Rectangle().adjust(shapeWidth: shapeWidth, shapeHeight: shapeHeight, angle: Angle(degrees: Double(i) * Double(angleStep))))
case 1:
Capsule().adjust(shapeWidth: shapeWidth, shapeHeight: shapeHeight, angle: Angle(degrees: Double(i) * Double(angleStep))))
case 2:
Ellipse().adjust(shapeWidth: shapeWidth, shapeHeight: shapeHeight, angle: Angle(degrees: Double(i) * Double(angleStep))))
default:
Rectangle().adjust(shapeWidth: shapeWidth, shapeHeight: shapeHeight, angle: Angle(degrees: Double(i) * Double(angleStep))))
}
}
}

Related

Round specific border corner with SwiftUI

I am trying to add a border to a view and round the topLeading and topTrailing corner only. It seems extremely difficult to achieve? It's easy enough to just round the corners with this extension:
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape( RoundedCorner(radius: radius, corners: corners) )
}
}
But this does not work when you apply a stroke. Any ideas how to achieve this?
The common way to add a border to a view in SwiftUI is via the .overlay() modifier. Using the RoundedCorner shape you've already made, we can modify this answer to create a new modifier that'll both round the shape and add a border.
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
extension View {
public func borderRadius<S>(_ content: S, width: CGFloat = 1, cornerRadius: CGFloat, corners: UIRectCorner) -> some View where S : ShapeStyle {
let roundedRect = RoundedCorner(radius: cornerRadius, corners: [.topLeft, .topRight])
return clipShape(roundedRect)
.overlay(roundedRect.stroke(content, lineWidth: width))
}
}
Usage:
Color.yellow
.borderRadius(Color.red, width: 15, cornerRadius: 25, corners: [.topLeft, .topRight])
.padding()
.frame(width: 300, height: 150)

How would you make Icons like in the Settings App (SwiftUI)

How could you achieve Icons like that?
I know that the base is this:
Image(systemName: "person.fill")
And than you could give it a background-Color:
Image(systemName: "person.fill")
.background(Color.blue)
To get rounded corners you could just add cornerRadius:
Image(systemName: "person.fill")
.background(Color.blue)
.cornerRadius(5)
But how would you make it that each of the items is in a square box with the same size?
Because SF Symbols don't have the same size.
And I don't want to make this:
Image(systemName: "person.fill")
.frame(width: 20, height: 20)
.background(Color.blue)
.cornerRadius(5)
The frame modifier would destroy the ability of SF Symbols to match with the preferred Font Size of the User.
Is there an other solution?
Or do you think the Settings App is done with .frame()?
Okay, I found an answer at Medium.
He works with Labels and adds an custom Modifier to them.
The Modifier looks like that:
struct ColorfulIconLabelStyle: LabelStyle {
var color: Color
var size: CGFloat
func makeBody(configuration: Configuration) -> some View {
Label {
configuration.title
} icon: {
configuration.icon
.imageScale(.small)
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 7 * size).frame(width: 28 * size, height: 28 * size).foregroundColor(color))
}
}
}
I did some changes:
struct ColorfulIconLabelStyle: LabelStyle {
var color: Color
func makeBody(configuration: Configuration) -> some View {
Label {
configuration.title
} icon: {
configuration.icon
.font(.system(size: 17))
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 7).frame(width: 28, height: 28).foregroundColor(color))
}
}
}
You can use it like that:
NavigationLink {
//Destination
} label: {
Label("Your Text", systemImage: "Your Image").labelStyle(ColorfulIconLabelStyle(color: .green))
}
This achieves a very native look :)
As I mentioned, full credits to Luca J.
I recommend this down way for you the update for my answer would be reading the device is zoomed or not! Then we could gave correct size for your UI, you can change your wished size in class.
struct ContentView: View {
var body: some View {
CustomButtonView(string: "gear", action: { print("setting!") })
CustomButtonView(string: "lasso.sparkles", action: { print("lasso!") })
CustomButtonView(string: "xmark.bin", action: { print("xmark!") })
CustomButtonView(string: "command", action: { print("command!") })
CustomButtonView(string: "infinity", action: { print("infinity!") })
}
}
struct CustomButtonView: View {
let string: String
let action: (() -> Void)?
init(string: String, action: (() -> Void)? = nil) {
self.string = string
self.action = action
}
#State private var tapped: Bool = Bool()
var body: some View {
Image(systemName: string)
.resizable()
.scaledToFit()
.frame(width: DeviceReader.shared.size - 5.0, height: DeviceReader.shared.size - 5.0)
.foregroundColor(Color.white)
.padding(5.0)
.background(tapped ? Color.blue : Color.gray)
.cornerRadius(10.0)
.onTapGesture { tapped.toggle(); action?() }
.animation(.interactiveSpring(), value: tapped)
}
}
class DeviceReader: ObservableObject {
let size: CGFloat
init() {
switch UIDevice.current.userInterfaceIdiom {
case .phone: self.size = 30.0
case .pad: self.size = 40.0
case .mac: self.size = 50.0
default: self.size = 30.0 }
}
static let shared: DeviceReader = DeviceReader()
}
I was looking at this question and your answer to it because this would be a useful thing to have. However, your answer does not allow the SFFont to scale with user preferences, and the answer you found on the Medium post does not scale well, as you can't just scale up and down with theses things. They look weird. If you run it in the simulator and change the Text setting, your will see what I mean.
I would simply use a .frame that changes it's size based off a preference key on the SF Symbol itself, and giving it a bit of padding extra. You could also simply add .padding() before your .background(), but the background would not necessarily be square. This method will set the width and height of the frame to slightly more than the biggest dimension of the SF Symbol, and it will fluidly change its size, not only allowing you to drop a .font() on it, but also handle the dynamic font sizes. This is a pure SwiftUI answer, using no UIKit.
struct ColoredIconView: View {
let imageName: String
let foregroundColor: Color
let backgroundColor: Color
#State private var frameSize: CGSize = CGSize(width: 30, height: 30)
#State private var cornerRadius: CGFloat = 5
var body: some View {
Image(systemName: imageName)
.overlay(
GeometryReader { proxy in
Color.clear
.preference(key: SFSymbolKey.self, value: max(proxy.size.width, proxy.size.height))
}
)
.onPreferenceChange(SFSymbolKey.self) {
let size = $0 * 1.05
frameSize = CGSize(width:size, height: size)
cornerRadius = $0 / 6.4
}
.frame(width: frameSize.width, height: frameSize.height)
.foregroundColor(foregroundColor)
.background(
RoundedRectangle(cornerRadius: cornerRadius)
.fill(backgroundColor)
)
}
}
fileprivate struct SFSymbolKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
Use it like this:
ColoredIconView(imageName: "airplane", foregroundColor: .white, backgroundColor: .orange)
.font(.body)

SwiftUI - onPreferenceChange is not called

I was trying to understand how preference and onPreferenceChange works, so I was experimenting with a few hierarchies, unfortunately I am stuck right now on one situation where onPreferenceChange is not called anymore.
What is strange to me, is fact, when I (on the bottom of TestView struct) remove lines
.modifier(Border(.column))
OR
Spacer(minLength: 0)
onPreferenceChange starts working normaly/is called as I expect
I was trying to find explanation, but with no success. Can anyone explain to me, what I am doing wrong?
Here is code of my root view
public struct MyRootView: View {
public var body: some View {
TestView()
.onPreferenceChange(ChangeInfoKey.self) { changeInfo in
print("preference changed: \(changeInfo)")
}
}
}
TestView:
struct TestView: View {
#State private var changeInfo: ChangeInfo?
var body: some View {
VStack {
Group {
Button(action: {
self.changeInfo = ChangeInfo(title: "Some Title \(Int.random(in: 0...10))")
print("Change initiated: \(self.changeInfo!.title) - \(self.changeInfo!.id)")
}) {
Text("My Button")
}
.preference(key: ChangeInfoKey.self, value: changeInfo)
}
.modifier(Border(.column))
Spacer(minLength: 0)
}
}
}
Border modifier creates only visual border around view, I am not sure, if its relevant with this issue, but for those, who want to see completely all parts of my code, or try it:
struct Border: ViewModifier {
enum BorderType: RawRepresentable {
case section
case emptyColumn
case column
case widget
case debug
var rawValue: (CGFloat, CGFloat, Color) {
switch self {
case .section: return (2, 0, Color.blue)
case .emptyColumn: return (0.5, 4, Color(0xa7afb5))
case .column: return (0.5, 4, Color(0xa7afb5))
case .widget: return (0.5, 2, Color(0xa7afb5))
case .debug: return (4, 0, Color.red)
}
}
init?(rawValue: (CGFloat, CGFloat, Color)) {
fatalError("ElementBorder.init(rawValue) - not implemented")
}
}
let lineWidth: CGFloat
let color: Color
let dashLength: CGFloat
init(_ borderType: BorderType) {
self.lineWidth = borderType.rawValue.0
self.dashLength = borderType.rawValue.1
self.color = borderType.rawValue.2
}
func body(content: Content) -> some View {
if dashLength > 0 {
return AnyView(content
.overlay(
Rectangle()
.strokeBorder(style: StrokeStyle(lineWidth: self.lineWidth, dash: [self.dashLength]))
.foregroundColor(self.color)
))
} else {
return AnyView(content
.border(self.color, width: self.lineWidth)
)
}
}
}

Downsize but not upsize a SwiftUI image

I have a list of profile images (of various sizes) that I want each one to downsize to fit into its view, but I don't want them to be upsized and be pixilated. Instead, I want small images to stay at the resolution they are at.
How do I can achieve this?
This is what I've been using so far (but it resizes up):
VStack {
Image(...)
.resizable()
.scaledToFill()
}.frame(width:200, height:200)
I did not find simple solution in API either, so here is a placeholder that looks appropriate for me. It is a bit complicated by works.
Tested with Xcode 11.2+ / iOS 13.2+.
Demo of usage:
struct DemoImagePlaceholder_Previews: PreviewProvider {
static var previews: some View {
VStack {
ImagePlaceholder(image: Image("icon"), size: CGSize(width: 200, height: 200))
.border(Color.red)
ImagePlaceholder(image: Image("large_image"), size: CGSize(width: 200, height: 200))
.border(Color.red)
}
}
}
Solution:
struct OriginalImageRect {
var rect: Anchor<CGRect>? = nil
}
struct OriginalImageRectKey: PreferenceKey {
static var defaultValue: OriginalImageRect = OriginalImageRect()
static func reduce(value: inout OriginalImageRect, nextValue: () -> OriginalImageRect) {
value = nextValue()
}
}
struct ImagePlaceholder: View {
let image: Image
let size: CGSize
var body: some View {
VStack {
self.image.opacity(0)
.anchorPreference(key: OriginalImageRectKey.self, value: .bounds) {
OriginalImageRect(rect: $0)
}
}
.frame(width: size.width, height: size.height)
.overlayPreferenceValue(OriginalImageRectKey.self) { pref in
GeometryReader { gp -> Image in
if pref.rect != nil, CGRect(origin: .zero, size: gp.size).contains(gp[pref.rect!]) {
return self.image
} else {
return self.image.resizable() // .fill by default, otherwise needs to wrap in AnyView
}
}
}
}
}

Activity indicator in SwiftUI

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.
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:
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)
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
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)
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.
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()
}
}
}
}
}
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()
}
}
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)
}
}
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)
}
}
It's really easy with SwiftUI 2.0 I made this simple and easy custom view with ProgressView
Here is how it looks:
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))
}
}
}
A convenient way in SwiftUI that I found useful is 2 step approach:
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)
}
}
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
}
}
}
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.
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)
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
// 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)
}
}
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()
}
}
Result of Basic Activity Indicator :