Created
September 27, 2024 08:06
-
-
Save iandreyshev/ee79e75583e15dc4bb2bafc2c6ace8a6 to your computer and use it in GitHub Desktop.
SwiftUI architecture example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ContentView.swift | |
// SwiftUIArchitectureSample | |
// | |
// Created by Ivan Andreyshev on 26.09.2024. | |
// | |
import SwiftUI | |
// View экрана | |
// | |
// Задачи: | |
// - Рисует экран | |
// | |
// Максимально глупый компонент: только рисует дизайн по стейту и передает все события | |
// от пользователя во ViewModel, чтобы та принимала решения по изменению стейта | |
// | |
// Важно чтобы поле viewModel отмечалось аннотацией @StateObject | |
// с помощью этого будет происходить перерисовка экрана при изменении состояния | |
// | |
struct ContentView: View { | |
@StateObject | |
private var viewModel = CalcViewModel() | |
var body: some View { | |
VStack { | |
if viewModel.state.isLoadingOnStart { | |
ProgressView() | |
Spacer().frame(height: 20) | |
Text("Пожалуйста, подождите, калькулятор включается...") | |
.multilineTextAlignment(.center) | |
} else { | |
if viewModel.state.isCalculating { | |
ProgressView() | |
} else { | |
Text(viewModel.state.result) | |
.font(.title) | |
} | |
Spacer().frame(height: 10) | |
Text("Калькулятор считает сумму чисел") | |
Spacer().frame(height: 40) | |
VStack { | |
HStack(spacing: 20) { | |
numberButtonView(number: 1) | |
numberButtonView(number: 2) | |
numberButtonView(number: 3) | |
} | |
Spacer().frame(height: 20) | |
HStack(spacing: 20) { | |
numberButtonView(number: 4) | |
numberButtonView(number: 5) | |
numberButtonView(number: 6) | |
} | |
Spacer().frame(height: 20) | |
HStack(spacing: 20) { | |
numberButtonView(number: 7) | |
numberButtonView(number: 8) | |
numberButtonView(number: 9) | |
} | |
Spacer().frame(height: 20) | |
numberButtonView(number: 0) | |
Spacer().frame(height: 20) | |
Button("Посчитать сумму") { viewModel.onSubmit() } | |
.font(.title) | |
Spacer().frame(height: 20) | |
Button("Сброс") { viewModel.onReset() } | |
.font(.title) | |
} | |
} | |
} | |
.onAppear { | |
viewModel.onFirstLoading() | |
} | |
} | |
func numberButtonView(number: Int) -> some View { | |
return Button(String(number)) { viewModel.onAddNumber(number) } | |
.font(.title) | |
.frame(width: 30) | |
} | |
} | |
// Состояние экрана | |
// | |
// Задачи: | |
// - хранить данные для отображения | |
// - хранить данные для определения бизнес логики | |
// | |
struct CalcState { | |
var isLoadingOnStart = true | |
var numbers: [Int] = [] | |
var result = "Введите числа" | |
var isCalculating = false | |
} | |
// ViewModel экрана | |
// | |
// Задачи: | |
// - Содержит бизнес-логику экрана (в простейшем случае) | |
// - Получает вызовы от View и принимает решение что с ними делать | |
// - Обновляет состояние экрана | |
// | |
// Любой ввод от пользователя попадает во ViewModel и внутри нее происходит | |
// какая-то логика, а затем изменение состояния | |
// | |
// Важно чтобы ViewModel наследовалась от ObservableObject, а State отмечался | |
// аннотацией @Published | |
// | |
class CalcViewModel: ObservableObject { | |
@Published | |
var state: CalcState = .init() | |
func onFirstLoading() { | |
// Task используется когда нужно выполнить длительные операции, чтобы | |
// не блокировать UI-поток и интерфейс не зависал | |
Task { | |
// Имитация первой загрузки | |
try await Task.sleep(nanoseconds: UInt64(3 * Double(NSEC_PER_SEC))) | |
state.isLoadingOnStart = false | |
} | |
} | |
func onAddNumber(_ number: Int) { | |
if state.isCalculating { | |
return | |
} | |
state.numbers.append(number) | |
state.result = state.numbers | |
.map { String($0) } | |
.joined(separator: " + ") | |
} | |
func onSubmit() { | |
// Запрещаем что-то делать, если вычисление уже в процессе | |
if state.isCalculating { | |
return | |
} | |
state.isCalculating = true | |
// Task используется когда нужно выполнить длительные операции, чтобы | |
// не блокировать UI-поток и интерфейс не зависал | |
Task { | |
if state.numbers.isEmpty { | |
state.result = "Числа не вводились" | |
} else { | |
// Имитация | |
// Как будто наше вычисление делается долго | |
try await Task.sleep(nanoseconds: UInt64(3 * Double(NSEC_PER_SEC))) | |
var sum = 0 | |
state.numbers.forEach { | |
sum += $0 | |
} | |
state.result = "Сумма чисел: \(sum)" | |
state.numbers = [] | |
} | |
state.isCalculating = false | |
} | |
} | |
func onReset() { | |
if state.isCalculating { | |
return | |
} | |
state.numbers = [] | |
state.result = "Введите числа" | |
} | |
} | |
#Preview { | |
ContentView() | |
} |
150 if -> guard
var sum = 0
state.numbers.forEach {
sum += $0
}
let sum = state.numbers.reduce(0, +)
179 if -> guard
CalcState -> CalculationContext / CalculatorData
Spacer().frame(height: 10) так лучше не делать, посмотри в сторону Grid
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
.onAppear -> .task