3
\$\begingroup\$

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
                    )
                }
            }
        }
    }
}

\$\endgroup\$
1
\$\begingroup\$

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 ..< 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.

\$\endgroup\$
1
  • \$\begingroup\$ 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! \$\endgroup\$ – fewlinesofcode Sep 3 '20 at 7:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.