# SwiftUI Dot indicator

I want to understand if I got this concept correctly.

1. I should start work from ViewState; a reference type object holding the state of a view. Here it is called PageIndicatorVM. It is bad-to-impossible to have any internal state in a View.
2. No logic in the View or so they say. Yet here and there in the code samples I see ternary operators and loops.
3. @Bidindg property wrapper seems quite dangerous for me. It breaks unidirectional data flow and I have doubts if I used it correctly here.

Please point out my mistakes. Will appreciate if you point me to best practices.

This is a dot indicator component. It switches when vm.next() is called or user taps on a dot.

struct DotIndicator: View {
let pageIndex: Int

@Binding var isOn: Int

var body: some View {

Button(action: {
self.isOn = self.pageIndex
}) {
Circle()
.scaleEffect( isOn == pageIndex ? 1.3 : 0.9)
.animation(.spring())
}

}
}

class PageIndicatorVM: ObservableObject {
@Published var currentPage: Int

let numPages: Int

init(numPages: Int, currentPage: Int = 0) {
self.numPages = numPages
self.currentPage = currentPage
}

func next() {
currentPage = (currentPage + 1) % numPages
}
}

struct PageIndicator: View {
@ObservedObject private var vm = PageIndicatorVM(numPages: 5)

private let spacing: CGFloat = 2
private let dotSize: CGFloat = 8

var body: some View {
VStack {
HStack(alignment: .center, spacing: spacing) {
ForEach((0..<vm.numPages ), id: \.self) {
DotIndicator(pageIndex: $0, isOn: self.$vm.currentPage)
.frame(
width: self.dotSize,
height: self.dotSize
)
}
}
}
}
}



It is bad-to-impossible to have any internal state in a View.

Actually SwiftUI was designed to handle state changes directly in a view. For simple cases you can easily do:

struct PageIndicator: View {
@State private var currentPage = 0
let numPages: Int = 5

private let spacing: CGFloat = 2
private let dotSize: CGFloat = 8

var body: some View {
VStack {
HStack(alignment: .center, spacing: spacing) {
ForEach(0 ..< numPages, id: \.self) {
DotIndicator(pageIndex: $0, isOn: self.$currentPage)
.frame(width: self.dotSize, height: self.dotSize)
}
}
}
}
}

PageIndicator(numPages: 5)


What's more sometimes an @ObservedObject may be reinitialised contrary to a @State property (unless you use a @StateObject available in SwiftUI 2.0).

For more information see: What is the difference between ObservedObject and StateObject in SwiftUI.

No logic in the View or so they say. Yet here and there in the code samples I see ternary operators and loops.

True, but for simple cases creating a whole new ViewModel can actually make code less readable (as in the example above). Note that a ForEach loop is perfectly valid in a view.

Apart from the comments above you can use an Image(systemName:) instead of a Circle:

struct DotIndicator: View {
let pageIndex: Int

@Binding var isOn: Int

var body: some View {
Button(action: {
self.isOn = self.pageIndex
}) {
Image(systemName: "circle.fill")
.imageScale(.small)
.scaleEffect(isOn == pageIndex ? 1.0 : 0.7)
.animation(.spring())
}
}
}


Because a Circle consumes all available space, you need to constrain it with the .frame modifier. You don't have to do that with an Image - it reduces the code (dotSize is no longer needed) and scales automatically.

• Thank You for the review! I had been working with SwiftUI for a while after this post and now i am comfortable with it. Agree with all your comments! Sep 3 '20 at 7:16