How would you make Icons like in the Settings App (SwiftUI) - 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)

Related

How to dynamically get view's origin in swiftui

Background
Using SwiftUI, I want to implement tutorial(Coach marks) functionalities for some view( like button, text, image and so on) which was defined without specifying origin and sizes
for modifier frame. especially such functionalities will be asked for several difference screen.
My Test Code
1. Definition of PreferenceKey
struct MyPreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
main view
struct ContentView: View {
#State var rating : Int = 3
var body: some View {
GeometryReader { geometry in
let origin = geometry.frame(in: .global).origin
let size = geometry.frame(in: .global).size
VStack {
HStack{
Text("Test1") // for testing following target,just add some view hierachy
.padding()
.background(Color.yellow)
VStack{
Text("test2")
.padding()
.background(Color.purple)
Text("") // here is my target, i want to get the origin of this view
.frame(width: 100, height: 200) // just for having spece to show value of frame
.background(Color.green)
.overlay(
GeometryReader { proxy in
let frm = proxy.frame(in: .global)
let size = proxy.size
Color.clear.preference(key: MyPreferenceKey.self,value: proxy.frame(in: .global))
Text(verbatim: "X=\(String(format: "%.2f", frm.midX)) y=\(String(format: "%.2f", frm.midY) ) width=\(String(format: "%.2f", frm.width)) height=\(String(format: "%.2f", frm.height))")
}
)
}
}
.onPreferenceChange(MyPreferenceKey.self){
print("\($0)")
}
}
.frame(width: 200, height: 300, alignment: .center)
}
}
}
3. Ran result (Xcode)
4.Ran result (Log)
(89.83333333333331, 127.16666666666669, 100.0, 200.0)
5. Problem
the view with green background says it's origin.x is 139, but logs printed above is 89 in modifier onPreferenceChange.
My Question:
how can i get real origin.x with 139 using like modifier onPreferenceChange.
is there a way to get value shown by above Text view
thanks.

SwiftUI Dynamic Image Sizing

Problem:I have a View that I needed to place multiple (2) views that contained: 1 Image + 1 Text. I decided to break that up into a ClickableImageAndText structure that I called on twice. This works perfectly if the image is a set size (64x64) but I would like this to work on all size classes. Now, I know that I can do the following:
if horizontalSizeClass == .compact {
Text("Compact")
} else {
Text("Regular")
}
but I am asking for both Different Size Classes and Same Size Classes such as the iPhone X and iPhone 13 which are the same.
Question:How do I alter the image for dynamic phone sizes (iPhone X, 13, 13 pro, etc) so it looks appropriate for all measurements?
Code:
import SwiftUI
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
var body: some View {
VStack {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 64, height: 64)
Text(text)
.foregroundColor(.white)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
struct InitialView: View {
var topView: some View {
Image("Empty_App_Icon")
.resizable()
.scaledToFit()
}
var bottomView: some View {
VStack {
ClickableImageAndText(
image: "Card_Icon",
text: "View Your Memories") {
print("Tapped on View Memories")
}
.padding(.bottom)
ClickableImageAndText(
image: "Camera",
text: "Add Memories") {
print("Tapped on Add Memories")
}
.padding(.top)
}
}
var body: some View {
ZStack {
GradientView()
VStack {
Spacer()
topView
Spacer()
bottomView
Spacer()
}
}
}
}
struct InitialView_Previews: PreviewProvider {
static var previews: some View {
InitialView()
}
}
Image Note:My background includes a GradientView that I have since removed (thanks #lorem ipsum). If you so desire, here is the GradientView code but it is unnecessary for the problem above.
GradientView.swift
import SwiftUI
struct GradientView: View {
let firstColor = Color(uiColor: UIColor(red: 127/255, green: 71/255, blue: 221/255, alpha: 1))
let secondColor = Color(uiColor: UIColor(red: 251/255, green: 174/255, blue: 23/255, alpha: 1))
let startPoint = UnitPoint(x: 0, y: 0)
let endPoint = UnitPoint(x: 0.5, y: 1)
var body: some View {
LinearGradient(gradient:
Gradient(
colors: [firstColor, secondColor]),
startPoint: startPoint,
endPoint: endPoint)
.ignoresSafeArea()
}
}
struct GradientView_Previews: PreviewProvider {
static var previews: some View {
GradientView()
}
}
Effort 1:Added a GeometryReader to my ClickableImageAndText structure and the view is automatically changed incorrectly.
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
var body: some View {
GeometryReader { reader in
VStack {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 64, height: 64)
Text(text)
.foregroundColor(.white)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
}
Effort 2:Added a GeometryReader as directed by #loremipsum's [deleted] answer and the content is still being pushed; specifically, the topView is being push to the top and the bottomView is taking the entire space with the addition of the GeometryReader.
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
var body: some View {
GeometryReader{ geo in
VStack {
Image(image)
.resizable()
.scaledToFit()
//You can do this and set strict size constraints
//.frame(minWidth: 64, maxWidth: 128, minHeight: 64, maxHeight: 128, alignment: .center)
//Or this to set it to be percentage of the size of the screen
.frame(width: geo.size.width * 0.2, alignment: .center)
Text(text)
}.foregroundColor(.white)
//Everything moves to the left because the `View` expecting a size vs stretching.
//If yo want the entire width just set the View with on the outer most View
.frame(width: geo.size.width, alignment: .center)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
The possible solution is to use screen bounds (which will be different for different phones) as reference value to calculate per-cent-based dynamic size for image. And to track device orientation changes we wrap our calculations into GeometryReader.
Note: I don't have your images, so added white borders for demo purpose
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
#State private var size = CGFloat(32) // some minimal initial value (not 0)
var body: some View {
VStack {
Image(image)
.resizable()
.scaledToFit()
// .border(Color.white) // << for demo !!
.background(GeometryReader { _ in
// GeometryReader is needed to track orientation changes
let sizeX = UIScreen.main.bounds.width
let sizeY = UIScreen.main.bounds.height
// Screen bounds is needed for reference dimentions, and use
// it to calculate needed size as per-cent to be dynamic
let width = min(sizeX, sizeY)
Color.clear // % (whichever you want)
.preference(key: ViewWidthKey.self, value: width * 0.2)
})
.onPreferenceChange(ViewWidthKey.self) {
self.size = max($0, size)
}
.frame(width: size, height: size)
Text(text)
.foregroundColor(.white)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}

SwiftUI onTapGesture on Color.clear background behaves differently to Color.blue

I am making a custom Picker in the SegmentedPickerStyle(). I want to have the same behaviour but when I tap on the area between the content and the border of one of the possible selections the onTapGesture does not work. When I add a blue background it does work but with a clear background it doesn't.
Working with blue background
Not working with clear background
Not working code:
import SwiftUI
struct PickerElementView<Content>: View where Content : View {
#Binding var selectedElement: Int
let content: () -> Content
#inlinable init(_ selectedElement: Binding<Int>, #ViewBuilder content: #escaping () -> Content) {
self._selectedElement = selectedElement
self.content = content
}
var body: some View {
GeometryReader { proxy in
self.content()
.fixedSize(horizontal: true, vertical: true)
.frame(minWidth: proxy.size.width, minHeight: proxy.size.height)
// ##################################################################
// CHANGE COLOR HERE TO BLUE TO MAKE IT WORK
// ##################################################################
.background(Color.clear)
// ##################################################################
.border(Color.yellow, width: 5)
}
}
}
struct PickerView: View {
#Environment (\.colorScheme) var colorScheme: ColorScheme
var elements: [(id: Int, view: AnyView)]
#Binding var selectedElement: Int
#State var internalSelectedElement: Int = 0
private var width: CGFloat = 220
private var height: CGFloat = 100
private var cornerRadius: CGFloat = 20
private var factor: CGFloat = 0.95
private var color = Color(UIColor.systemGray)
private var selectedColor = Color(UIColor.systemGray2)
init(_ selectedElement: Binding<Int>) {
self._selectedElement = selectedElement
self.elements = [
(id: 0, view: AnyView(PickerElementView(selectedElement) {
Text("9").font(.system(.title))
})),
(id: 1, view: AnyView(PickerElementView(selectedElement) {
Text("5").font(.system(.title))
})),
]
self.internalSelectedElement = selectedElement.wrappedValue
}
func calcXPosition() -> CGFloat {
var pos = CGFloat(-self.width * self.factor / 4)
pos += CGFloat(self.internalSelectedElement) * self.width * self.factor / 2
return pos
}
var body: some View {
ZStack {
Rectangle()
.foregroundColor(self.selectedColor)
.cornerRadius(self.cornerRadius * self.factor)
.frame(width: self.width * self.factor / CGFloat(self.elements.count), height: self.height - self.width * (1 - self.factor))
.offset(x: calcXPosition())
.animation(.easeInOut(duration: 0.2))
HStack {
ForEach(self.elements, id: \.id) { item in
item.view
.gesture(TapGesture().onEnded { _ in
print(item.id)
self.selectedElement = item.id
withAnimation {
self.internalSelectedElement = item.id
}
})
}
}
}
.frame(width: self.width, height: self.height)
.background(self.color)
.cornerRadius(self.cornerRadius)
.padding()
}
}
struct PickerView_Previews: PreviewProvider {
static var previews: some View {
PickerView(.constant(1))
}
}
Change the color where I marked it.
Does anyone know why they behave differently and how I can fix this?
The one line answer is instead of setting backgroundColor, please set contentShape for hit testing.
var body: some View {
GeometryReader { proxy in
self.content()
.fixedSize(horizontal: true, vertical: true)
.frame(minWidth: proxy.size.width, minHeight: proxy.size.height)
// ##################################################################
// CHANGE COLOR HERE TO BLUE TO MAKE IT WORK
// ##################################################################
.contentShape(Rectangle())
// ##################################################################
.border(Color.yellow, width: 5)
}
}
Transparent views are not tappable by default in SwiftUI because their content shape is zero.
You can change this behavior by using .contentShape modifier:
Color.clear
.frame(width: 300, height: 300)
.contentShape(Rectangle())
.onTapGesture { print("tapped") }
It appears to be a design decision that any Color with an opacity of 0 is untappable.
Color.clear.onTapGesture { print("tapped") } // will not print
Color.blue.opacity(0).onTapGesture { print("tapped") } // will not print
Color.blue.onTapGesture { print("tapped") } // will print
Color.blue.opacity(0.0001).onTapGesture { print("tapped") } // will print
You can use the 4th option to get around this, as it is visually indistinguishable from the 1st.
I was struggling a similar problem to get the tap on a RoundedRectangle.
My simple solution was to set the opacity to a very low value and it worked
RoundedRectangle(cornerRadius: 12)
.fill(Color.black)
.opacity(0.0001)
.frame(width: 32, height: 32)
.onTapGesture {
...
}

How to create Radiobuttons in SwiftUI?

I would like to react on a choice of a user. Something similar to this example:
In a 2nd stage would I like to show additional content below each radiobutton, e.g. moving the buttons 2 and 3 from each other in order to give a list of websites for allowing.
So far I haven't found how to do this in SwiftUI.
Many thanks in advance!
Picker(selection: $order.avocadoStyle, label: Text("Avocado:")) {
Text("Sliced").tag(AvocadoStyle.sliced)
Text("Mashed").tag(AvocadoStyle.mashed)
}.pickerStyle(RadioGroupPickerStyle())
This is the code from the 2019 swiftUI essentials keynote (SwiftUI Essentials - WWDC 2019. Around 43 minutes in the video they show this example.
It will look like this:
check this out...an easy to use SwiftUI RadiobuttonGroup for iOS
you can use it like this:
RadioButtonGroup(items: ["Rome", "London", "Paris", "Berlin", "New York"], selectedId: "London") { selected in
print("Selected is: \(selected)")
}
and here is the code:
struct ColorInvert: ViewModifier {
#Environment(\.colorScheme) var colorScheme
func body(content: Content) -> some View {
Group {
if colorScheme == .dark {
content.colorInvert()
} else {
content
}
}
}
}
struct RadioButton: View {
#Environment(\.colorScheme) var colorScheme
let id: String
let callback: (String)->()
let selectedID : String
let size: CGFloat
let color: Color
let textSize: CGFloat
init(
_ id: String,
callback: #escaping (String)->(),
selectedID: String,
size: CGFloat = 20,
color: Color = Color.primary,
textSize: CGFloat = 14
) {
self.id = id
self.size = size
self.color = color
self.textSize = textSize
self.selectedID = selectedID
self.callback = callback
}
var body: some View {
Button(action:{
self.callback(self.id)
}) {
HStack(alignment: .center, spacing: 10) {
Image(systemName: self.selectedID == self.id ? "largecircle.fill.circle" : "circle")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: self.size, height: self.size)
.modifier(ColorInvert())
Text(id)
.font(Font.system(size: textSize))
Spacer()
}.foregroundColor(self.color)
}
.foregroundColor(self.color)
}
}
struct RadioButtonGroup: View {
let items : [String]
#State var selectedId: String = ""
let callback: (String) -> ()
var body: some View {
VStack {
ForEach(0..<items.count) { index in
RadioButton(self.items[index], callback: self.radioGroupCallback, selectedID: self.selectedId)
}
}
}
func radioGroupCallback(id: String) {
selectedId = id
callback(id)
}
}
struct ContentView: View {
var body: some View {
HStack {
Text("Example")
.font(Font.headline)
.padding()
RadioButtonGroup(items: ["Rome", "London", "Paris", "Berlin", "New York"], selectedId: "London") { selected in
print("Selected is: \(selected)")
}
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ContentViewDark_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.colorScheme, .dark)
.darkModeFix()
}
}
I just edited #LizJ answer , by adding Binding instead of didTapActive & didTapInactive , so like that it will looks like other SwiftUI elements
import SwiftUI
struct RadioButton: View {
#Binding var checked: Bool //the variable that determines if its checked
var body: some View {
Group{
if checked {
ZStack{
Circle()
.fill(Color.blue)
.frame(width: 20, height: 20)
Circle()
.fill(Color.white)
.frame(width: 8, height: 8)
}.onTapGesture {self.checked = false}
} else {
Circle()
.fill(Color.white)
.frame(width: 20, height: 20)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.onTapGesture {self.checked = true}
}
}
}
}
I'm using swift4, Catalina OS and Xcode 11.2 and was having the issue where RadioGroupPickerStyle was unavailable for iOS and .radiogroup just didn't work (it froze in build) so I made my own that's reusable for other occasions. (notice its only the button so you have to handle the logic yourself.) Hope it helps!
import SwiftUI
struct RadioButton: View {
let ifVariable: Bool //the variable that determines if its checked
let onTapToActive: ()-> Void//action when taped to activate
let onTapToInactive: ()-> Void //action when taped to inactivate
var body: some View {
Group{
if ifVariable {
ZStack{
Circle()
.fill(Color.blue)
.frame(width: 20, height: 20)
Circle()
.fill(Color.white)
.frame(width: 8, height: 8)
}.onTapGesture {self.onTapToInactive()}
} else {
Circle()
.fill(Color.white)
.frame(width: 20, height: 20)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.onTapGesture {self.onTapToActive()}
}
}
}
}
TO USE: Put this in any file and you can use it as you would any other view anywhere else in the project. (we keep a global folder that has a buttons file in it)
I will use the previous answer of #LizJ and i will add a text after the radio button to resemble (RadioListTile in Flutter)
struct RadioButton: View {
let ifVariable: Bool //the variable that determines if its checked
let radioTitle: String
var onTapToActive: ()-> Void//action when taped to activate
let onTapToInactive: ()-> Void //action when taped to inactivate
var body: some View {
Group{
if ifVariable {
HStack(alignment: .center, spacing: 16) {
ZStack{
Circle()
.fill(AppColors.primaryColor)
.frame(width: 20, height: 20)
Circle()
.fill(Color.white)
.frame(width: 8, height: 8)
}.onTapGesture {self.onTapToInactive()}
Text(radioTitle)
.font(.headline)
}
} else {
HStack(alignment: .center, spacing: 16){
Circle()
.fill(Color.white)
.frame(width: 20, height: 20)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.onTapGesture {self.onTapToActive()}
Text(radioTitle)
.font(.headline)
}
}
}
}
I will also provide an example for the selection logic
we will create a enum for radio cases
enum PaymentMethod: Int {
case undefined = 0
case credit = 1
case cash = 2
}
then we will create #State variable to carry the selection, i will not recreate another SwiftUI view but only explain the basic concept without any boilerplate code
struct YourView: View {
#State private var paymentMethod: PaymentMethod
var body: some View {
RadioButton(ifVariable: paymentMethod == PaymentMethod.credit,radioTitle: "Pay in Credit", onTapToActive: {
paymentMethod = .credit
}, onTapToInactive: {})
RadioButton(ifVariable: paymentMethod == PaymentMethod.cash,radioTitle: "Pay in Cash", onTapToActive: {
paymentMethod = .cash
}, onTapToInactive: {})
}
}
with this previous code you can toggle between radio buttons in SwiftUI with a text after each selection to resemble (RadioListTile in Flutter)

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 :