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 Answer 1

    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\$CommentedSep 3, 2020 at 7:16

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.