Table of Contents
Introduction
Objective - C is serving Apple platforms for more than 3 decades and it is a very mature language but in 2014 when Apple announced a new programming language “Swift”.
- Objective-C without C.
- No more square brackets.
- No more semicolons.
- One of the fastest-growing languages.
That delighted the whole apple community with Joy.
We experience the same joy when Apple announced a new way to write UI code. “SwiftUI”.
- Declarative way to write code.
- All swift.
- No more storyboard merge conflicts.
- No more build, check, edit loops.
- More extensible and maintainable code.
- Same Code/Technique to all Apple platforms.
The more we think about, the more we love.
And the best way to learn about SwiftUI is to see it in action by building an app. In this post, we are going to create Clocky (Analog Clock) in SwiftUI from scratch.
Basic Introduction to SwiftUI
To start with SwiftUI, one intuitive way to get started is to have an analogy.
Here we are showing some of the most used UIKit classes with the equivalent in SwiftUI.
UIKitSwiftUIUIViewView (a protocol)UIScrollViewScrollViewUITableViewListViewUICollectionViewNo native CollectionView yetUIViewControllerEverything is a ViewCG DrawingShape (a protocol)UIButtonButtonUIImageViewImageUILabelTextUIGestureRecogniserGesture (a protocol)StackViewHStack, VStack, ZStackUINavigationControllerNavigationViewetc.
All SwiftUI Views are designed to be Value Types. So we need to adjust our Reference Type perspective of UIKit while working with SwiftUI. So we might have the following questions.
Storyboard vs Code?
With Storyboards, we need to choose between the benefits of using a visual editor or the benefits of creating your UI in code. And if we choose one and change our mind later, then we have to start all over again.
In SwiftUI, now our view definition is always Swift's code and we can always choose to edit the code directly or to use the visual editor. And you can always go back and forth. So if we select something in the Canvas, then that selection is reflected in the code as well. And if we change something in the code, then that change is reflected in the Canvas as well. Even Apple baked SwiftUI by keeping Human Interface guidelines for all platforms in mind.
How does SwiftUI fit in the UIKit world?
Since any iOS application's root is UIWindow where we can set our rootViewController. How do we set our SwiftUI view in the window's rootViewController?
UIKit provides a bridging class called UIHostingViewController. You can see the usage in SceneDelegate.swift file.
class UIHostingController<Content> : UIViewController where Content : View { … }
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
So you can wrap your SwiftUI Views in your existing UIKit code with the help of UIHostingViewControllers. (There are other ways to inter-operate between UIKit & SwiftUI.)
Let’s create an Analog Clock using SwiftUI.
- Start by creating a new Xcode Project by selecting SwiftUI for the User-Interface option.
- Save the project to your desired location.
Here we can divide the clock UI into 4 parts.
- Background
- Ticks
- Texts (12, 1, 2, 3, …)
- Hands (Hour, Minute, Seconds)
In this post, we will use the following features.
- Opaque Types
- Custom Shape Drawing using Shape protocol
- Property wrapper
- Publisher for Timer.
Background
This is the simplest part of the whole UI. Our output will look like this.
struct ContentView: View {
//1
var body: some View {
Circle() //(2)
//3
.fill(Color.red)
//4
.padding()
}
}
Here is what code doing step by step.
Here you may be unfamiliar with keyword some. This is a new type in Swift 5.1 called Opaque Types, thanks to Swift Evolution Proposal SE-0244
- Opaque types preserve type identity, unlike returning a value whose type is a protocol type.
- An opaque type refers to one specific type, although the caller of the function isn’t able to see which type; a protocol type can refer to any type that conforms to the protocol.
Circle() is a type for drawing Circles in View.
- SwiftUI provides several Shapes like Circle, Capsule, Rectangle, RoundedRectangle.
- We can draw our custom shape by following Shape protocol.
.fill. We call these kinds of methods modifiers and they're used in SwiftUI to customize the way our views look or behave.
- Here fill is used with Color style to have a red background.
- We can have different ShapeStyle like LinearGradient, RadialGradient, AngularGradient.
.padding will add some spacing to the whole view. Here we are not specifying any value so it will use default values as per Human Interface Guidelines.
- We can completely ignore SafeArea by .padding(edgesIgnoringSafeArea(.all)) or any Edge.
- .padding(.leading, 8) will add spacing from leading of 8.
- .padding(8) will add spacing from all edges of 8.
So we have completed the first part of Clocky.
Ticks
We will create a struct Ticks which fulfills the protocol shape. We are trying to draw ticks using Path. Path is equivalent to UIBezierPath/CGPath in UIKit. We will draw a stroke for all ticks. To stroke a path we will need a start point and endpoint. We will calculate these points using Polar to Cartesian Transform We are also trying to have longer ticks for Texts 12,1,2,3,... so we will define the property for these heights as well..
struct Ticks: Shape {
//1
let inset: CGFloat
//2
let minTickHeight: CGFloat
//3
let hourTickHeight: CGFloat
//4
let totalTicks = 60
//5
let hourTickInterval: Int = 5
//6
func path(in rect: CGRect) -> Path {
let rect = rect.insetBy(dx: inset, dy: inset)
var path = Path()
for index in 0..<totalTicks {
let condition = index % hourTickInterval == 0
let height: CGFloat = condition ? hourTickHeight : minTickHeight
path.move(to: topPosition(for: angle(for: index), in: rect))
path.addLine(to: bottomPosition(for: angle(for: index), in: rect, height: height))
}
return path
}
//7
private func angle(for index: Int) -> CGFloat {
return (2 * .pi / CGFloat(totalTicks)) * CGFloat(index)
}
//8
private func topPosition(for angle: CGFloat, in rect: CGRect) -> CGPoint {
let radius = min(rect.height, rect.width)/2
let xPosition = rect.midX + (radius * cos(angle))
let yPosition = rect.midY + (radius * sin(angle))
return CGPoint(x: xPosition, y: yPosition)
}
//9
private func bottomPosition(for angle: CGFloat, in rect: CGRect, height: CGFloat) -> CGPoint {
let radius = min(rect.height, rect.width)/2
let xPosition = rect.midX + ((radius - height) * cos(angle))
let yPosition = rect.midY + ((radius - height) * sin(angle))
return CGPoint(x: xPosition, y: yPosition)
}
}
Here is what code doing step by step.
- inset is used to have some margin for the ticks to start drawing.
- minTickHeight is used to have the height for minute ticks.
- hourTickHeight is used to have a large height for hour ticks.
We are drawing a total of 60 ticks.
- One tick for every minute and a longer tick at 5th interval.
In 60 ticks every 5 ticks will be hour tick.
- So this constant will be used to decide which tick has to be longer the iteration.
This is the required method for Shape protocol, where we stroke the path for each tick.
- For drawing the strokes for each tick, we iterate through totalTicks constant and making deciding height for every tick.
- Now we are moving our path's start point to topPosition calculated according to angle for current tick.
- Then we are adding a line to the endpoint by calling the bottom position.
- And iteration continues for the next tick till the last one.
We calculate the angle of each tick for its position.
- Here we have a total of 2π or 360° for a complete circle.
- We are drawing a total of 60 ticks so each tick will be separated by 2π/60 radians or 360°/60degrees.
- So individual tick angle can be calculated by 2π/60 * indexOfTick.
Based on the angle we calculate the start position for the tick stroke.
- To calculate the position using Polar to Cartesian transform we calculated center and radius.
- The end position of tick stroke.
Texts
Let’s create another struct for the Texts TickText.The output will look like this.
To create a view like this we can have 12 Text in a loop and positioned according to its index as we did in Ticks. To get the frame in view , we need to use Geometry Reader to get the frame in the local Coordinate space.
struct TickText: View {
//1
var ticks: [String]
var inset: CGFloat
//2
private struct IdentifiableTicks: Identifiable {
var id: Int
var tick: String
}
//3
private var dataSource: [IdentifiableTicks] {
self.ticks.enumerated().map { IdentifiableTicks(id: $0, tick: $1) }
}
var body: some View {
//4
GeometryReader { proxy in
ZStack {
ForEach(self.dataSource) {
Text("\($0.tick)")
.position(
//5
self.position(for: $0.id, in: proxy.frame(in: .local))
)
}
}
}
}
//6
private func position(for index: Int, in rect: CGRect) -> CGPoint {
let rect = rect.insetBy(dx: inset, dy: inset)
let angle = ((2 * .pi) / CGFloat(ticks.count) * CGFloat(index)) - .pi/2
let radius = min(rect.width, rect.height)/2
return CGPoint(x: rect.midX + radius * cos(angle),
y: rect.midY + radius * sin(angle))
}
}
private func position(for index: Int, in rect: CGRect) -> CGPoint { //(6)
let rect = rect.insetBy(dx: inset, dy: inset)
let angle = ((2 * .pi) / CGFloat(ticks.count) * CGFloat(index)) - .pi/2
let radius = min(rect.width, rect.height)/2
return CGPoint(x: rect.midX + radius * cos(angle),
y: rect.midY + radius * sin(angle))
}
}
Here is what code doing step by step.
- Our dataSource for the 12,1,2,3,... texts.
- We convert the ticks array to IdentifiableTicks easy to work with ForEach and positioning each Text because ForEach requires Identifiable type to iterate.
- Computed property to convert between ticks array to IdentifiableTicks array.
- We need to use GeometryReader type to get the frame for our content. that we use for the position each Text.
- Position the text by reading the frame from GeometryReader's proxy in the CoordinateSpace.local.
- Finally, we calculate the position for each Text using formula Polar to Cartesian coordinates.
X = center.x + radius * cos(angle), Y = center.y + radius * sin(angle)
Hands
For hands we need 3 hands for Hour, Minute and Seconds So we can define a Single struct for Hand following the Shape protocol. Our final output will look like this.
Hand
struct Hand: Shape {
let inset: CGFloat
//1
let angle: Angle
//2
func path(in rect: CGRect) -> Path {
let rect = rect.insetBy(dx: inset, dy: inset)
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addRoundedRect(in: CGRect(x: rect.midX - 4, y: rect.midY - 4, width: 8, height: 8), cornerSize: CGSize(width: 8, height: 8))
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addLine(to: position(for: CGFloat(angle.radians), in: rect))
return path
}
//3
private func position(for angle: CGFloat, in rect: CGRect) -> CGPoint {
let angle = angle - (.pi/2)
let radius = min(rect.width, rect.height)/2
let xPosition = rect.midX + (radius * cos(angle))
let yPosition = rect.midY + (radius * sin(angle))
return CGPoint(x: xPosition, y: yPosition)
}
}
Here is what the code is doing step by step.
- Since this Hand type will be used for Hour, Minute and Seconds hand we need a dependency angle for each hand. To manage easily with Degrees and Radians, SwiftUI provides Angle type.
- Protocol requirement of Shape, where we stroke according to angle.
- We calculate the position based on the angle.
TickHands
struct TickHands: View {
//1
@State private var currentDate = Date()
//2
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
var body: some View {
//3
ZStack {
Hand(inset: 50, angle: currentDate.hourAngle)
.stroke(lineWidth: 4)
.foregroundColor(.black)
Hand(inset: 22, angle: currentDate.minuteAngle)
.stroke(lineWidth: 4)
.foregroundColor(.black)
Hand(inset: 10, angle: currentDate.secondAngle)
.stroke(lineWidth: 2)
.foregroundColor(.gray)
}
//4
.onReceive(timer) { (input) in
self.currentDate = input
}
}
}
Here is what the code doing step by step.
- We create a property for currentDate using @State property wrapper. Which will notify our body to redraw on change.
- To update our hands we use Timer Publisher (A type in Combine Framework) used to notify event changes. In our case timer.
- We use ZStack to layout our Hour, Minute and Seconds hands on top of each other.
- We receive the timer updates and update currentDate that will trigger to re-render our TickHandsview.
Glue all pieces together
Here we have built our UI in components. It's time glue all components in ContentView.
struct ContentView: View {
var body: some View {
//1
ZStack {
//2
Circle()
.fill(Color.red)
//3
Ticks(inset: 8, minTickHeight: 10, hourTickHeight: 20)
.stroke(lineWidth: 2)
.foregroundColor(.white)
//4
TickText(
ticks: [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map{"\($0)"},
inset: 45
)
.foregroundColor(.white)
.font(Font.system(size: 27))
//5
TickHands()
}
//6
.padding()
}
}
Here is what code doing step by step.
- We use ZStack to layout all our pieces in Z-axis.
- We create Circle() for the background
- Create Ticks bypassing all the dependencies
- The TickText on top of Ticks.
- Finally TickHands for Hour, Minute and Seconds hands.
- We declare padding for all our ZStack. This modifier will propagate to all children of ZStack and apply padding to all.
Clocky
Final Thoughts
Here we developed a working analog clock purely using SwiftUI, some Swift5.1 features and little of Combine Framework. We hope you enjoyed this post. Finally, we want to thank all Awesome engineers of the SwiftUI framework at Apple.
· · · ·Third Rock Techkno is a leading IT services company. We are a top-ranked web, voice and mobile app development company with over 10 years of experience. Client success forms the core of our value system.
We have expertise in the latest technologies including angular, react native, iOs, Android and more. Third Rock Techkno has developed smart, scalable and innovative solutions for clients across a host of industries.
Our team of dedicated developers combine their knowledge and skills to develop and deliver web and mobile apps that boost business and increase output for our clients.