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 calledPageIndicatorVM
. It is bad-to-impossible to have any internal state in aView
. - 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..<vm.numPages ), id: \.self) {
DotIndicator(pageIndex: 0,ドル isOn: self.$vm.currentPage)
.frame(
width: self.dotSize,
height: self.dotSize
)
}
}
}
}
}
1 Answer 1
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.
-
\$\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– fewlinesofcode2020年09月03日 07:16:37 +00:00Commented Sep 3, 2020 at 7:16