{"componentChunkName":"component---src-templates-article-js","path":"/article/swiftui-ring-animation","result":{"data":{"contentfulArticle":{"title":"Apple Watch Rings","image":"https://res.cloudinary.com/liquidcoder/image/upload/v1597488342/RingGraph/ob8q7h0thtl4aathhooi.png","createdDate":"Friday, 25 October 2019","videoUrl":"bLH4BvySawU","updatedDate":"3 years ago","isNewLayout":null,"previews":null,"slug":"swiftui-ring-animation","author":{"email":"john@liquidcoder.com","name":"John"},"tags":[{"title":"Article"},{"title":"Free"},{"title":"Swift UI"},{"title":"UI Design"}],"allChapters":null,"code":null,"explanation":null,"downloads":[{"name":"Project source code","url":"https://github.com/liquidcoder1/RingGraph/archive/master.zip"}],"resources":[{"name":"Apple developer documentation","url":"https://developer.apple.com/documentation/swiftui"},{"name":"Hacking with swift","url":"https://www.hackingwithswift.com"},{"name":"Swift by sundell","url":"https://www.swiftbysundell.com"}],"body":{"childMdx":{"tableOfContents":{"items":[{"url":"#introduction","title":"Introduction"},{"url":"#requirements","title":"Requirements"},{"url":"#get-started","title":"Get started"},{"url":"#ringshape","title":"RingShape"},{"url":"#ringview","title":"RingView"},{"url":"#putting-everything-together","title":"Putting everything together"},{"url":"#conclusion","title":"Conclusion"}]},"body":"function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, [\"components\"]);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"h2\", null, \"Introduction\"), mdx(\"p\", null, \"This article will be focused on working with paths. You will learn how to \"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"create custom shapes with paths\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"add gradients colours to custom shapes\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Animate custom shapes\")), mdx(\"h2\", null, \"Requirements\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Xcode 12 or greater\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"MacOS Catalina or greater\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Swift 5.0 or greater\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"iOS 14 or greater\")), mdx(\"h2\", null, \"Get started\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Open Xcode\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Create a new Xcode project\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Select single view app and click next\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Name your app and make sure the user interface is Swift UI\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Last, click Finish.\")), mdx(\"p\", null, \"With that out of the way, Let\\u2019s get going, shall we ?!\"), mdx(\"p\", null, \"Rename the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ContentView\"), \" filename and the struct to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"RingGraph\"), \", and make sure you rename its reference in \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"SceneDelegate\")), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-swift\"\n  }), \"func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {\\n    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.\\n    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.\\n    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).\\n\\n    // Create the SwiftUI view that provides the window contents.\\n :: let ringGraph = RingGraph()::\\n\\n    // Use a UIHostingController as window root view controller.\\n    if let windowScene = scene as? UIWindowScene {\\n        let window = UIWindow(windowScene: windowScene)\\n        window.rootViewController = ::UIHostingController(rootView: weatherApp)::\\n        self.window = window\\n        window.makeKeyAndVisible()\\n    }\\n}\\n\")), mdx(\"p\", null, \"Change everywhere you see \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ContentView\"), \" referenced to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"RingGraph\"), \".\"), mdx(\"h2\", null, \"RingShape\"), mdx(\"p\", null, \"Create a folder named Shapes and inside it create a swift file named \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"RingShape\"), \".\\nCopy the following code and paste it in:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-swift\"\n  }), \"import SwiftUI\\n\\nstruct RingShape: Shape {\\n    var percent: Double\\n    var radius: CGFloat = 100\\n    \\n    var animatableData: Double{\\n        get{\\n            return percent\\n        }\\n        \\n        set{\\n            percent = newValue\\n        }\\n    }\\n\\n    \\n    func path(in rect: CGRect) -> Path {\\n        let width = rect.width\\n        let height = rect.height\\n        let center = CGPoint(x: width / 2, y: height / 2)\\n        let endAngle = Angle(degrees: ( percent / 100 * 360) - 90)\\n        let radius = width / 2\\n        \\n        return Path{ path in\\n            path.addArc(center: center, radius: radius, startAngle: Angle(degrees: -90.0) , endAngle: endAngle, clockwise: false)\\n        }\\n    }\\n}\\n\\n\")), mdx(\"p\", null, \" \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Explanation\")), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"To create a custom path, the struct need to conform to  the \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"Shape\"), \" protocol.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"The first 2 properties are obvious.  As for the third property , we need that to animate the drawing of the path. Every time you need to animate a path, make sure to override that property and return the property that will be changing the state of the path to be animated.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"When you conform to the \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"Shape\"), \" protocol, you will be required to override this method  \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"path(in rect: CGRect) -> Path\"), \".\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"In the path method, I set up constants to be used to draw the arc. Notice the -90 value. I subtract that value because I want the drawing to start from 90 degrees rather than 0 (default).\")), mdx(\"h2\", null, \"RingView\"), mdx(\"p\", null, \"Now, it\\u2019s time to create a view that will hold our newly created path.\\nCreate a new folder named Views and inside it create a swiftUI file named \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"RingView\"), \". \"), mdx(\"p\", null, \"Replace the content of  that file with the following code:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-swift\"\n  }), \"import SwiftUI\\n\\nstruct Ring: View {\\n    @Binding var percent: Double\\n    var thickness: CGFloat = 35\\n    var fontSize:CGFloat = 15\\n    var gradientColors =  [Color.blue, Color.red]\\n    \\n    var body: some View {\\n        return drawRing()\\n    }\\n    \\n    private func drawRing() -> some View{\\n        let formattedPercent = String(format: \\\"%.f\\\", CGFloat(self.percent))\\n        \\n        return ZStack(alignment: .top) {\\n            \\n            RingShape(percent: 100)\\n                .stroke(style: StrokeStyle(lineWidth: self.thickness - 5))\\n                .fill(Color.gray.opacity(0.2))\\n            \\n            RingShape(percent: self.percent)\\n                .stroke(style: StrokeStyle(lineWidth: self.thickness, lineCap: CGLineCap.round))\\n                .fill(\\n                    LinearGradient(\\n                        gradient: .init(colors: gradientColors), startPoint: .init(x: 0.2, y: 0.4), endPoint:  .init(x: 0.5, y: 1)\\n                    )\\n            )\\n            \\n            Text(\\\"\\\\(formattedPercent)%\\\")\\n                .multilineTextAlignment(.trailing)\\n                .font(.system(size: fontSize, weight: .black))\\n                .offset(y: -thickness / 4)\\n                .shadow(radius: 10)\\n        }\\n    }\\n}\\n\\nstruct Ring_Previews: PreviewProvider {\\n    static var previews: some View {\\n        Ring(percent: .constant(50))\\n    }\\n}\\n\\n\")), mdx(\"p\", null, \"This code is also straight forward. The bit to note here is how I create 2 \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"RingPaths\"), \"; the first one is a full circle with a grey colour and the second is the one that will be indicating the percentage level. Try playing with the startPoint and endPoint to see how it works.\\n\", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Preview:\")), mdx(\"p\", null, mdx(\"img\", _extends({\n    parentName: \"p\"\n  }, {\n    \"src\": \"https://res.cloudinary.com/liquidcoder/image/upload/v1571775169/RingGraph/rfbx7jymcxulkmow5ebp.png\",\n    \"alt\": null\n  }))), mdx(\"h2\", null, \"Putting everything together\"), mdx(\"p\", null, \"If you haven\\u2019t downloaded the \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://github.com/liquidcoder1/RingGraph/archive/master.zip\"\n  }), \"completed project\"), \" linked above, you should do it now in order to use the colours that I\\u2019ve used. Just find the \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Assets.xcassets\"), \"  folder  and copy the colours folder. \"), mdx(\"p\", null, \"Create a new folder named \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Utils\"), \" and create a swift file in it named \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Colors\"), \". Add the following block of code in that file:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-swift\"\n  }), \"import SwiftUI\\n\\n\\nextension Color {\\n    static var ring1color1: Color {\\n        return Color(\\\"ring1color1\\\")\\n    }\\n    static var ring1color2:Color {\\n        return Color(\\\"ring1color2\\\")\\n    }\\n    static var ring2color1:Color {\\n        return Color(\\\"ring2color1\\\")\\n    }\\n    static var ring2color2:Color {\\n        return Color(\\\"ring2color2\\\")\\n    }\\n    static var ring3color1:Color {\\n        return Color(\\\"ring3color1\\\")\\n    }\\n    static var ring3color2:Color {\\n        return Color(\\\"ring3color2\\\")\\n    }\\n}\\n\\n\")), mdx(\"p\", null, \"Then create a new folder named \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"Modifiers\"), \" and add a file named \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"NutrientModifier\"), \" containing the following block of code:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-swift\"\n  }), \"\\nimport SwiftUI\\n\\nstruct NutrientModifier: ViewModifier {\\n    var color: Color = .red\\n    func body(content: Content) -> some View {\\n           content.foregroundColor(color)\\n           .frame(width: 25, height: 25)\\n           .cornerRadius(4)\\n    }\\n\\n}\\n\\n\")), mdx(\"p\", null, \"This is just a simple modifier that we will use in a short while.\"), mdx(\"p\", null, \"In the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"RingGraph\"), \" file, add these at the top inside the struct:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-swift\"\n  }), \"    @State var percent1: Double = 60\\n    @State var percent2: Double = 70\\n    @State var percent3: Double = 80\\n    \\n    var gRing1:[Color] = [Color.ring1color1, Color.ring1color2]\\n    var gRing2:[Color] = [Color.ring2color1, Color.ring2color2]\\n    var gRing3:[Color] = [Color.ring3color1, Color.ring3color2]\\n    \\n    private var thickness: CGFloat = 40\\n\")), mdx(\"p\", null, \"In the body, remove whatever is there and put this code inside\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-swift\"\n  }), \"NavigationView {\\n                VStack {\\n                    \\n                    Text(\\\"You have consumed \\\\(String(format: \\\"%.1f\\\", CGFloat((self.percent1 + self.percent2 + self.percent3) / 3)))% today\\\")\\n                        .font(.title)\\n                        .fontWeight(.bold)\\n                        .lineLimit(2)\\n                        .multilineTextAlignment(.center)\\n                        .padding(.horizontal, 30)\\n                        .frame(height: 70)\\n                    \\n                    Text(\\\"Perfectly balanced \\\\n like everything should be.\\\")\\n                        .multilineTextAlignment(.center)\\n                        .padding(.bottom, 30)\\n \\n                     // This creates the 3 rings graphs   \\n                    self.createGrapth().frame(minWidth: 0.0, maxWidth: .infinity)\\n                    Spacer()\\n                    HStack {\\n                        HStack{\\n                            Rectangle().modifier(NutrientModifier(color: .ring1color1) )\\n                            Text(\\\"Carb\\\")\\n                            \\n                        }\\n                        Spacer()\\n                        HStack{\\n                            Rectangle().modifier(NutrientModifier(color: .ring2color2) )\\n                            Text(\\\"Protein\\\")\\n                            \\n                        }\\n                        Spacer()\\n                        HStack{\\n                            Rectangle().modifier(NutrientModifier(color: .ring3color2) )\\n                            Text(\\\"Fat\\\")\\n                        }\\n                    }\\n                    \\n                }.padding().navigationBarTitle(Text(\\\"\\\"), displayMode: .inline).navigationBarItems(trailing: self.trailingButton())\\n            \\n        }\\n\")), mdx(\"p\", null, \"This code is self-explanatory, you will get an error because there\\u2019s no \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"createGraph\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"trailingButton\"), \" functions yet. Let\\u2019s now create  those:\"), mdx(\"p\", null, \"Put this inside the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"RingGraph\"), \" struct but below the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"body\"), \" block:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-swift\"\n  }), \"private func createGraph() -> some View{\\n        let width = UIScreen.main.bounds.width - 20\\n        return\\n            ZStack {\\n                RingView(percent: self.$percent1, thickness: self.thickness, fontSize: 15, gradientColors: gRing1).frame(width: width - thickness, height: width - thickness )\\n                RingView(percent: self.$percent2, thickness:  self.thickness, fontSize: 15, gradientColors: gRing2).frame(width: width - thickness * 3, height: width - thickness * 3)\\n                RingView(percent: self.$percent3, thickness:  self.thickness, fontSize: 15, gradientColors: gRing3).frame(width: width - thickness * 5, height: width - thickness * 5)\\n            }\\n    }\\n    \\n    private func trailingButton() -> some View{\\n          return Button(action: {\\n              withAnimation(.easeInOut(duration: 1)) {\\n                  self.percent1 = Double.random(in: 1...100)\\n                  self.percent2 = Double.random(in: 1...100)\\n                  self.percent3 = Double.random(in: 1...100)\\n              }\\n          }) {\\n              Image(systemName: \\\"arrow.clockwise\\\")\\n                  .resizable()\\n                  .frame(width: 25, height: 30)\\n                  .aspectRatio(contentMode: ContentMode.fit)\\n          }\\n      }\\n\")), mdx(\"p\", null, \"As you can see, those are 2 separate functions. The first one creates the 3 rings and the second one. generates 3 random numbers between 1 and 100 for each of the rings.\"), mdx(\"h2\", null, \"Conclusion\"), mdx(\"p\", null, \"That\\u2019s it folks\\u2026. The \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"RingView\"), \" can be used in any scenario you can think of. Feel free to play with the code, modify it if you want, you\\u2019ll learn better that way. Happy coding. Enjoy!!!\"));\n}\n;\nMDXContent.isMDXComponent = true;","excerpt":"Introduction This article will be focused on working with paths. You will learn how to  create custom shapes with paths add gradients colours to custom shapes Animate custom shapes Requirements Xcode 12 or greater MacOS Catalina or greater Swift 5.0 or greater iOS 14 or greater Get started Open…"}}}},"pageContext":{"slug":"swiftui-ring-animation"}},"staticQueryHashes":["1047155515","1278771865","2517639938","2517639938","2537039069","3452127246","3566879862","3891201089","439617394","670651827"]}