SwiftUI Dot indicator I want to understand if I got this concept correctly.

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.
No logic in the View or so they say. Yet here and there in the code samples I see ternary operators and loops.
@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..&lt;vm.numPages ), id: \.self) {
                    DotIndicator(pageIndex: $0, isOn: self.$vm.currentPage)
                    .frame(
                        width: self.dotSize,
                        height: self.dotSize
                    )
                }
            }
        }
    }
}
Your code is perfectly fine. Just a couple of comments:

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 ..&lt; 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: &quot;circle.fill&quot;)
                .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.