Skip to content

Commit

Permalink
Worked through data flow in SwiftUI apps.
Browse files Browse the repository at this point in the history
  • Loading branch information
adamahrens committed Jan 20, 2022
1 parent ebb6711 commit 72b2df9
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ struct KuchiApp: App {
WindowGroup {
StarterView()
.environmentObject(userManager)
.environmentObject(ChallengesViewModel())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,65 @@ struct ChallengeView: View {

@State var showAnswers = false

// Will be reworked soon
// @State var numberOfAnswered = 0

@Binding var numberOfAnswered: Int


// A way to subscribe to changes of the Environment
// Here it stores layout, color theme, etc.
// https://developer.apple.com/documentation/swiftui/environmentvalues
@Environment(\.verticalSizeClass) var verticalSizeClass

// You need @ViewBuilder because body can potentially return multiple views.
@ViewBuilder
var body: some View {
if verticalSizeClass == .compact {
VStack {
HStack {
Button(action: {
showAnswers = !showAnswers
}) {
QuestionView(
question: challengeTest.challenge.question)
}
if showAnswers {
Divider()
ChoicesView(challengeTest: challengeTest)
}
}
ScoreView(
numberOfAnswered: $numberOfAnswered,
numberOfQuestions: 5
)
}
} else {
VStack {
Button(action: {
showAnswers = !showAnswers
}) {
QuestionView(
question: challengeTest.challenge.question)
.frame(height: 300)
}
ScoreView(
numberOfAnswered: $numberOfAnswered,
numberOfQuestions: 5
)

if showAnswers {
Divider()
ChoicesView(challengeTest: challengeTest)
.frame(height: 300)
.padding()
}
}
}
}

/*
Old way
var body: some View {
VStack {
Button(action: {
Expand All @@ -46,7 +105,10 @@ struct ChallengeView: View {
.frame(height: 300)
}
ScoreView(numberOfQuestions: 5)
/* From previous example */
// ScoreView(numberOfAnswered: $numberOfAnswered, numberOfQuestions: 5)
ScoreView(numberOfAnswered: $numberOfAnswered, numberOfQuestions: 5)
if showAnswers {
Divider()
Expand All @@ -56,6 +118,7 @@ struct ChallengeView: View {
}
}
}
*/
}


Expand All @@ -69,7 +132,10 @@ struct ChallengeView_Previews: PreviewProvider {
answers: ["Thank you", "Hello", "Goodbye"]
)

@State static var numberOfAnswered = 0

static var previews: some View {
return ChallengeView(challengeTest: challengeTest)
return ChallengeView(challengeTest: challengeTest, numberOfAnswered: $numberOfAnswered)
.previewInterfaceOrientation(.landscapeLeft)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@ class ChallengesViewModel: ObservableObject {
]

var allAnswers: [String] { return Self.challenges.map { $0.answer }}
var correctAnswers: [Challenge] = []
var wrongAnswers: [Challenge] = []
var correctAnswers = [Challenge]()
var wrongAnswers = [Challenge]()

var numberOfAnswered: Int { return correctAnswers.count }

@Published var currentChallenge: ChallengeTest?

init() {
Expand Down Expand Up @@ -120,7 +121,6 @@ class ChallengesViewModel: ObservableObject {
var randomChallenges: Set<Challenge>

// If there are not enough challenges, return them all

if challenges.count < count {
randomChallenges = Set(challenges)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ struct ChoicesView : View {
let challengeTest: ChallengeTest
@State var challengeSolved = false
@State var isChallengeResultAlertDisplayed = false
@ObservedObject var challengesViewModel = ChallengesViewModel()

// Pull out of environment that was injected at the start
// Its important to note that when using .environment() modifier
// You don't specify a name. That means that the environment is essentially
// Just a bag which the ability to only have one of the same types.
@EnvironmentObject var challengesViewModel: ChallengesViewModel
// @ObservedObject var challengesViewModel = ChallengesViewModel()

var body: some View {
VStack(spacing: 25) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@
import SwiftUI

struct CongratulationsView: View {
@ObservedObject var challengesViewModel = ChallengesViewModel()
// @ObservedObject var challengesViewModel = ChallengesViewModel()

// Pull out from Environment
@EnvironmentObject var challengesViewModel: ChallengesViewModel

let avatarSize: CGFloat = 120
let userName: String

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ import SwiftUI
struct PracticeView: View {
@Binding var challengeTest: ChallengeTest?
@Binding var userName: String
@Binding var numberOfAnswered: Int

@ViewBuilder
var body: some View {
if challengeTest != nil {
ChallengeView(challengeTest: challengeTest!)
ChallengeView(challengeTest: challengeTest!, numberOfAnswered: $numberOfAnswered)
} else {
CongratulationsView(userName: userName)
}
Expand All @@ -57,7 +58,7 @@ struct PracticeView_Previews: PreviewProvider {
static var previews: some View {
return PracticeView(
challengeTest: .constant(challengeTest),
userName: .constant("Johnny Swift")
userName: .constant("Johnny Swift"), numberOfAnswered: $numberOfAnswered
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,23 @@ import SwiftUI

struct ScoreView: View {

@State var numberOfAnswered = 0
// @State var numberOfAnswered = 0

// We don't want ScoreView to be the Single Source of Truth
// We want that to exist outside of this view.
// @State var numberOfAnswered: Int

// But just doing @State isn't good enough. It is a pass by value
// Which means the values will get out of sync. Hence @Binding
// This is a dumb view so we want this owned by something else
@Binding var numberOfAnswered: Int

// Another way without using @State property wrapper
var answered = State<Int>(initialValue: 0)

// This Property isn't State because it doesnt
// change over the lifetime of the View. So make it a
// constant
let numberOfQuestions: Int

var body: some View {
Expand All @@ -49,8 +64,10 @@ struct ScoreView: View {
}

struct ScoreView_Previews: PreviewProvider {
@State static var numberOfAnswered: Int = 0

static var previews: some View {
ScoreView(numberOfAnswered: 0, numberOfQuestions: 5)
ScoreView(numberOfAnswered: $numberOfAnswered, numberOfQuestions: 5)
.previewLayout(.sizeThatFits)
.padding()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,24 @@ import SwiftUI

struct WelcomeView: View {
@EnvironmentObject var userManager: UserManager
@ObservedObject var challengesViewModel = ChallengesViewModel()

// Since this is the root it makes sense
// For this to hold the observable object
// Going for SSOT
// @ObservedObject var challengesViewModel = ChallengesViewModel()

// These are when using the modifier .environment() on a parent view
@EnvironmentObject var challengesViewModel: ChallengesViewModel

@State var showPractice = false

@ViewBuilder
var body: some View {
if showPractice {
PracticeView(
challengeTest: $challengesViewModel.currentChallenge,
userName: $userManager.profile.name
userName: $userManager.profile.name,
numberOfAnswered: .constant(challengesViewModel.numberOfAnswered)
)
} else {
ZStack {
Expand Down
38 changes: 37 additions & 1 deletion SwiftUI/SwiftUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,40 @@ Use `@Binding var yourData: Bool` instead of `@State` when you have reusable vie

Referring to `object.property` is read only. Using `$object.property` is a read-write binding

`Modifiers` take in a view, change it, and return a new view. Therefore the order in which you apply modifiers can make a difference. For example applying a `border` and then `padding` is different than `padding` and applying `border`
`Modifiers` take in a view, change it, and return a new view. Therefore the order in which you apply modifiers can make a difference. For example applying a `border` and then `padding` is different than `padding` and applying `border`

SwiftUI helps avoid the Massive View Controller anti pattern of UIKit. It does this because declarative, functional (same state produces same view), and reactive (updating state causes view to rerender)

The other benefit is that it creates a single source of truth. If you had a textfield and a name property in your model. Is the single source of truth the `textfield.text` or `name`? What if they get out of sync.

SwiftUI is then smart enough to only re render parts of the view that are affected by that `@State`

In SwiftUI, components don’t own the data — instead, they hold a reference to data that’s stored elsewhere. This is achieved with `@Binding`. Since a `TextField` doesn't want to own the data, it gets passed along to it.

```
You access a binding by using the $ operator
@State var name: String = ""
TextField("Type your name...", text: $name)
```

```
Using environmentObject(_:), you inject an object into the environment.
Using @EnvironmentObject, you pull an object (actually a reference to an object) out of the environment and store it in a property.
```

Helpful for maintaining a single source of truth.

Use `.constant(challengesViewModel.numberOfAnswered)` .constant if you have computed properties but want to give them binding like behavior.

Object ownership can be complicated with Reference types. Imagine if you pass a `UserManager` to the init of some view A. Whenever A is re-rendered a new `UserManager` is created and passed. It's preferred to store a single instance in a parent view and pass that instance to the child view.

A better way is to use `@StateObject` for refernce types now

```
struct SomeView: View {
@StateObject var userManager = UserManager()
...
}
```

0 comments on commit 72b2df9

Please sign in to comment.