Как я бросила двустороннюю архитектуру и ушла к Redux со SwiftUI и SwiftData

в 8:21, , рубрики: redux, swift, swiftdata, swiftUI, UDF, архитектура, с нуля

Вступление лирическое

Представьте себе ситуацию: вы освоили всё необходимое для работы, успешно работаете, проходит год, другой, и вдруг осознаёте, что, живя в своём уютном информационном пузыре, до сих пор не знаете, когда проходит WWDC или на какой версии Swift вы пишете

Да, я оказалась именно в таком пузыре. Всё вроде бы шло хорошо: задачи решались, меня хвалили, и не было никакой необходимости узнавать что-то новое. Конечно, я всё же чему-то училась, но точечно, по мере необходимости

И вот, пару раз споткнувшись в разговорах об свою ограниченность, прокринежевав потом от стыда, решила, что нужно что-то делать. Подписалась на кучу IOS каналов в телеграме, они стали моими мотиваторами к развитию не помню, как к этому пришла, но советую всем

До этого все мои приложения были написаны на UIKit, поняла что нужно переходить на SwiftUI. Придумала приложение, которое буду писать, решила использовать SwiftData для хранения данных. Почему бы и нет? Если уж начинать новый проект, так пусть он будет во всем новом

Долго выбирала архитектуру и остановилась на Redux, главная сложность была в разношерстной информации. Не хватало общей картины, и, хотя я находила полезные фрагменты, их было сложно сложить воедино, особенно подружить Redux со SwiftData. А как их объединять, если тычешь пальцем в небо? К тому же, хотелось заложить такую основу, чтобы приложение легко делилось на модули в будущем

В целом, я потратила не мало времени, чтобы разобрать и сформировать найденную информацию. И, естественно, это не эталон, я даже не претендую. Но, считаю, это хорошая теория для старта или тренажер для перестроения мышления на однонаправленную архитектуру

Вступление для тех кто торопится

На примере создания приложения pазберем архитектуру Redux, учтем возможное разделение приложения на модули. Будем постепенно наращивать уровень сложности (ориентир сложности: 🧠) В конце добавим работу со SwiftData, что закрепит понимание передачи данных не только в хранилище, но и по структурам Redux


Папки

Странно? Зачем начинать с расположения папок, и зачем вообще об этом говорить?

Как я бросила двустороннюю архитектуру и ушла к Redux со SwiftUI и SwiftData - 1

Я считаю, легче изучать тему от общего к частному, в нашем случае от структуры проекта к его реализации. Такое формирование структуры файлов не относится к Flux архитектуре. Здесь наша задача заложить фундамент для разделения приложения на модули в будущем. У меня получилось 4 абстрактных блока (папки)

  • App - общие файлы проекта, здесь располагается main (точка входа в приложение), а также элементы касающиеся приложения в целом. Эти элементы никогда не выносят в отдельные модули

  • Shared - это сервисы, утилиты, расширения, которые используются в разных частях приложения. В отличии от элементов папки App содержимое Shared можно вынести в отдельные модули или проекты, они никак не связаны с философией приложения, грубо говоря "чистое железо"

  • Features - хранит папки с фичами. Каждая фича в идеале должна быть изолирована от других фичей. Это позволит с легкостью выносить фичи в модули по надобности

  • Resources - содержит файлы с ресурсами для приложения в целом. Например, картинки, строки, информация с ключевыми настройками (.plist)

Основные термины Redux

В Redux есть термины, которые я до знакомства с архитектурой не встречала в программировании, но, возможно, я одна такая из пещеры вылезла...

  • State(системное) - текущее состояние, этим типом может быть Struct, Enum, Class или Primitive Type(Int, String, или Bool)

  • Action(системное)- описание того, что должно произойти, этим типом может быть Enum, Struct или Protocol

  • Reducer - функция, которая определяет, как изменяется состояние (State) в ответ на определённое действие (Action)

  • Store - объединяет в себе состояние (State) и связанный с ним Reducer

Разберем на самом простом примере. Создадим все эти элементы для экрана со списком задач, назовем фичу TaskList

Как я бросила двустороннюю архитектуру и ушла к Redux со SwiftUI и SwiftData - 2

Но сначала определим структуру расположения новых файлов

В папке фичи, создаем 3 новые директории:

Domain - содержит интерфейсное описание бизнес задачи, абстракция с моделями

Application - это также бизнес задачи, но уже их реализация, логика и представления, но без деталей

Infrastructure - низкоуровневые сервисы и утилиты, подобные элементам Shared, но относящиеся только к данной фиче

  1. Создадим TaskModel, тут все как обычно

struct TaskModel: Equatable, Identifiable {
	var id = UUID()
	var title: String
	var subtitle: String?
	var isCompleted = false
}
  1. Теперь создадим State, который будет содержать в себе массив TaskModel и свойство TaskListFilter, которое будет определять, как сортировать или фильтровать список задач. Обратите внимание, что структура TaskListState подписана только на Equatable, но выступает в роли State

enum TaskListFilter {
	case all
	case active
	case completed
}

struct TaskListState: Equatable {
	var tasks: [TaskModel] = []
	var filter: TaskListFilter = TaskListFilter.active

	var filteredTasks: [TaskModel] {
		switch filter {
		case .all:
			return tasks
		case .active:
			return tasks.filter { !$0.isCompleted }
		case .completed:
			return tasks.filter { $0.isCompleted }
		}
	}
}
  1. Создадим Action, это будут все возможные действия над нашими задачами. Обратите внимание, что enum TaskListAction не подписан на какие либо протоколы, но выступает в роли Action

enum TaskListAction {
	case addTask(TaskModel)
	case toggleTaskCompletion(UUID)
	case removeTask(UUID)
	case filterTasks(TaskListFilter)
}
  1. Создадим Reducer. Это функция, да! Без структур и классов, просто файл, в котором лежит функция. Она решает, как будет меняться State в зависимости от пришедшего в нее Action. Обратите внимание, что State приходит в эту функцию в качестве inout параметра

func taskListReducer(_ state: inout TaskListState, action: TaskListAction) {
	switch action {
	case .addTask(let task):
		state.tasks.append(task)
	case .toggleTaskCompletion(let id):
		if let index = state.tasks.firstIndex(where: { $0.id == id }) {
			state.tasks[index].isCompleted.toggle()
		}
	case .removeTask(let id):
		state.tasks.removeAll { $0.id == id }
	case .filterTasks(let filter):
		state.filter = filter
	}
}
  1. И последняя, соединяющая State и Action структура, а точнее класс. Здесь не требуется конкретная реализация под фичу TaskList, это будет дженерик класс. Напишем простейший из вариантов реализации:

Уровень сложности Store 🧠

class Store<State, Action>: ObservableObject where State: Equatable {
	@Published private(set) var state: State
	private private let reducer: (inout State, Action) -> Void

	init(initial state: State, reducer: @escaping (inout State, Action) -> Void) {
		self.reducer = reducer
		self.state = state
	}

	func dispatch(_ action: Action) {
		self.reducer(&self.state, action)
	}
}

Сначала сложно понять что куда и зачем. Во всем этом я вижу две задачи: предоставление информации и обработка этой информации действием

  1. Информация по состоянию нам всегда открыта, обращаемся к ней без дополнительных приседаний

  2. А вот изменять состояние мы должны только через метод dispatch в Store. Это позволяет реализовать дополнительный функционал обработки. Например, запросы в сеть, сохранение или логирование. Так же здесь ведется вся логика обработки действий

Цепочка, по которой проходит действие, прежде чем изменить State

Цепочка, по которой проходит действие, прежде чем изменить State

Добавим представление с возможностью удаления таски для мини демонстрации и вызовем его в main

struct TaskListView: View {

	@State var store: Store<TaskListState, TaskListAction>

	var body: some View {
		NavigationView {
			List {
				ForEach(store.state.filteredTasks) { task in
					Text(task.title)

				}
				.onDelete { indexSet in
					indexSet.map { store.state.tasks[$0].id
					}
					.forEach { id in
						store.dispatch(.removeTask(id))
					}
				}
			}
		}
	}
}

#Preview {
	TaskListView(
		store:
			Store(
				initial: TaskListState(
					tasks: [TaskModel(title: "Task 1"),
							TaskModel(title: "Task 2")]
				),
				reducer: taskListReducer
			)
	)
}

@main
struct TasksAlarmApp: App {

	var body: some Scene {
		WindowGroup {
			TaskListView(
				store: Store(
					initial: TaskListState(tasks: [
						TaskModel(title: "Task 1"),
						TaskModel(title: "Task 2")
					]),
					reducer: taskListReducer
				)
			)
		}
	}
}

Уровень сложности Store 🧠 🧠

Будем постепенно добавлять функционал в Store, идем от общего к частному, помните? Чтобы дойти до уровня, где мы будем уже внедрять SwiftData

Создаем рядом со Store. Observer.swift в Shared -> Flux

Observer - наблюдатель, с единственной открытой функцией исполнения. После исполнения наблюдатель может отписаться от наблюдения вернув ObserverStatus.dead

enum ObserverStatus {
	case alive
	case dead
}

final class Observer<State> {
	private let observeBlock: (State) -> ObserverStatus

	init(observe: @escaping (State) -> ObserverStatus) {
		self.observeBlock = observe
	}

	func observe(_ state: State) -> ObserverStatus {
		return observeBlock(state)
	}
}

// Позволяет использовать Observer в Set, что необходимо 
// для хранения наблюдателей в Store
extension Observer: Hashable {
	static func == (lhs: Observer<State>, rhs: Observer<State>) -> Bool {
		return lhs === rhs
	}

	func hash(into hasher: inout Hasher) {
		hasher.combine(ObjectIdentifier(self))
	}
}

Теперь усложняем Store:

class Store<State, Action>: ObservableObject where State: Equatable {
	@Published private(set) var state: State
	private let reducer: (inout State, Action) -> Void
	private var observers: Set<Observer<State>> = []

	init(initial state: State, reducer: @escaping (inout State, Action) -> Void) {
		self.reducer = reducer
		self.state = state
	}

	func dispatch(_ action: Action) {
		self.reducer(&self.state, action)
		self.notifyObservers()
	}

	func subscribe(observer: Observer<State>) {
		self.observers.insert(observer)
		self.notify(observer)
	}

	private func notifyObservers() {
		for observer in observers {
			notify(observer)
		}
	}

	private func notify(_ observer: Observer<State>) {
		if observer.observe(state) == .dead {
			observers.remove(observer)
		}
	}
}

Таким образом, мы расширили функционал хранилища. Что нового?

  • subscribe - ОТКРЫТАЯ функция для добавления наблюдателя (Observer)

  • Set<Observer<State>> - хранилище для наблюдателей

  • notifyObservers - функция оповещения наблюдателей, срабатывает после изменения State

  • notify - функция оповещения одного наблюдателя, срабатывает сразу после установки наблюдателя

Цепочка, по которой проходит действие, прежде чем изменить State, затем идет уведомление всех подписчиков об изменении состояния

Цепочка, по которой проходит действие, прежде чем изменить State, затем идет уведомление всех подписчиков об изменении состояния

Добавим наблюдателя в наш проект. Он будет срабатывать при каждой отрисовке View

struct TaskListView: View {
	@State var store: Store<TaskListState, TaskListAction>

	var body: some View {
		NavigationView {
			List {
				ForEach(store.state.filteredTasks) { task in
					Text(task.title)
				}
				.onDelete { indexSet in
					indexSet.map { store.state.tasks[$0].id }
						.forEach { id in
							store.dispatch(.removeTask(id))
						}
				}
			}
			.onAppear {
				store.subscribe(observer: Observer { newState in
					print("Состояние изменилось: (newState.tasks)")
					return .alive
				})
			}
		}
	}
}
Как я бросила двустороннюю архитектуру и ушла к Redux со SwiftUI и SwiftData - 5

При удалении каждой из задач меняется State, и Store уведомляет всех своих наблюдателей об этом

Что обычно может происходить в наблюдателях (Observer):

  • Обновление пользовательского интерфейса

  • Сохранение данных

  • Уведомление пользователя

  • Синхронизация с сервером

  • Обновление статистики или аналитики

Уровень сложности Store 🧠 🧠 🧠

Создаем Middleware.swift в Shared -> Flux

Middleware - это своего рода «посредник», который может делать что-то полезное с действиями до того, как оно достигнет редьюсера (reducer). Сюда могут входить:

  • Асинхронные запросы к API

  • Логирование действий

  • Обработка аутентификации

  • Изменение структуры данных

Реализация:

protocol Middleware {
	associatedtype State
	associatedtype Action

	func process(action: Action,
				 state: State,
				 next: @escaping (Action) -> Void)
}

struct AnyMiddleware<State, Action>: Middleware {
	private let _process: (Action, State, @escaping (Action) -> Void) -> Void

	init<M: Middleware>(_ middleware: M) where M.State == State, M.Action == Action {
		self._process = middleware.process
	}

	func process(action: Action,
				 state: State,
				 next: @escaping (Action) -> Void) {
		_process(action, state, next)
	}
}
class Store<State, Action>: ObservableObject where State: Equatable {
	@Published private(set) var state: State
	private let reducer: (inout State, Action) -> Void
	private var observers: Set<Observer<State>> = []
	private var middleware: [AnyMiddleware<State, Action>]

	init(
		initial state: State,
		reducer: @escaping (inout State, Action) -> Void,
		middleware: [AnyMiddleware<State, Action>] = []
	) {
		self.reducer = reducer
		self.state = state
		self.middleware = middleware
	}

	func dispatch(_ action: Action) {
		let middlewareChain = middleware.reversed().reduce({ action in
			self.reducer(&self.state, action)
			self.notifyObservers()
		}) { next, mw in
			return { action in
				mw.process(action: action, state: self.state, next: next)
			}
		}
		middlewareChain(action)
	}

	func subscribe(observer: Observer<State>) {
		self.observers.insert(observer)
		self.notify(observer)
	}

	private func notifyObservers() {
		for observer in observers {
			notify(observer)
		}
	}

	private func notify(_ observer: Observer<State>) {
		if observer.observe(state) == .dead {
			observers.remove(observer)
		}
	}
}

И вот тут снова у меня возникли сложности с пониманием, давайте разберем, что происходит в функции dispatch. Мы формируем цепочку из посредников (middleware) с конца в начало, передавая им еще не измененный State и ссылки на следующий (next) middleware. Описание комментариями в коде:

func dispatch(_ action: Action) {
    // Идем через middleware в обратном порядке, чтобы построить цепочку обработки
    let middlewareChain = middleware.reversed().reduce({ action in
        // Это последнее, что произойдет: изменим состояние через редьюсер и уведомим наблюдателей
        self.reducer(&self.state, action)
        self.notifyObservers()
    }) { next, mw in
        // Для каждого middleware создаем новый шаг в цепочке
        return { action in
            // Каждый middleware обрабатывает действие и затем передает его дальше
            mw(action, self.state, next)
        }
    }

    // Запускаем цепочку с первоначальным действием
    middlewareChain(action)
}

Я попыталась наглядно отобразить, как будет обрабатываться действие, которое идет по цепочке middleware. Пунктирные стрелки - это подробный вариант, для начала пройдитесь по основным стрелкам. На что обратить внимание:

  • State во все middleware записывается при создании цепочки, а значит во время выполнения не меняется

  • Парамерт next в последнем middleware ссылается на reduser

  • Каждый middleware принимает действие (action) и либо меняет его либо прерывает цепочку действий

Как я бросила двустороннюю архитектуру и ушла к Redux со SwiftUI и SwiftData - 6
Новые файлы

Новые файлы

Дополним наше приложение, применив улучшенный Store

struct TaskListEnhancement: Middleware {
	func process(action: TaskListAction, state: TaskListState, next: @escaping (TaskListAction) -> Void) {
		switch action {
		case .addTask(var task):
			// Если подзаголовок не указан, добавляем стандартный подзаголовок
			if task.subtitle == nil || task.subtitle?.isEmpty == true {
				task.subtitle = "No description provided"
			}
			let enhancedAction = TaskListAction.addTask(task)
			next(enhancedAction) // Передаем измененное действие дальше
		default:
			next(action) // Для других действий просто передаем действие дальше
		}
	}
}
struct TaskListValidation: Middleware {
	func process(action: TaskListAction, state: TaskListState, next: @escaping (TaskListAction) -> Void) {
		switch action {
		case .addTask(let task):
			guard !task.title.trimmingCharacters(in: .whitespaces).isEmpty else {
				print("TaskListValidation - Попытка добавить задачу с пустым названием отклонена")
				return // Отклоняем действие
			}
			next(action) // Если валидация пройдена, передаем действие дальше
		default:
			next(action) // Для других действий просто передаем действие дальше
		}
	}
}
TaskListView
struct TaskListView: View {
	@State var store: Store<TaskListState, TaskListAction>

	@State private var showSheet = false
	@State private var newTaskTitle = ""
	@State private var newTaskSubtitle = ""

	var body: some View {
		NavigationView {
			ZStack {
				TaskList(store: store)

				AddTaskButton(showSheet: $showSheet)
			}
			.navigationTitle("Tasks")
			.sheet(isPresented: $showSheet) {
				NewTaskForm(
					showSheet: $showSheet,
					newTaskTitle: $newTaskTitle,
					newTaskSubtitle: $newTaskSubtitle,
					store: store
				)
			}
		}
	}
}

struct TaskList: View {
	var store: Store<TaskListState, TaskListAction>

	var body: some View {
		List {
			ForEach(store.state.filteredTasks) { task in
				Text(task.title)
			}
			.onDelete { indexSet in
				indexSet.map { store.state.tasks[$0].id }
					.forEach { id in
						store.dispatch(.removeTask(id))
					}
			}
		}
		.onAppear {
			store.subscribe(observer: Observer { newState in
				print("Состояние изменилось: (newState.tasks)")
				return .alive
			})
		}
	}
}

struct AddTaskButton: View {
	@Binding var showSheet: Bool

	var body: some View {
		VStack {
			Spacer()
			HStack {
				Spacer()
				Button(action: {
					showSheet = true
				}) {
					Image(systemName: "plus")
						.foregroundColor(.white)
						.font(.system(size: 24))
						.padding()
						.background(Color.black)
						.clipShape(Circle())
						.shadow(radius: 10)
				}
				.padding()
			}
		}
	}
}

struct NewTaskForm: View {
	@Binding var showSheet: Bool
	@Binding var newTaskTitle: String
	@Binding var newTaskSubtitle: String

	var store: Store<TaskListState, TaskListAction>

	var body: some View {
		VStack {
			Text("Новая задача")
				.font(.headline)
				.padding()

			TextField("Название задачи", text: $newTaskTitle)
				.textFieldStyle(RoundedBorderTextFieldStyle())
				.padding()

			TextField("Подзаголовок задачи", text: $newTaskSubtitle)
				.textFieldStyle(RoundedBorderTextFieldStyle())
				.padding()

			HStack {
				Button("Отмена") {
					showSheet = false
				}
				.padding()

				Spacer()

				Button("Добавить") {
					let newTask = TaskModel(title: newTaskTitle, subtitle: newTaskSubtitle)
					store.dispatch(.addTask(newTask))
					// Очистка полей
					newTaskTitle = ""
					newTaskSubtitle = ""
					showSheet = false
				}
				.padding()
			}
			.padding()
		}
		.padding()
	}
}

TasksAlarmApp
@main
struct TasksAlarmApp: App {

	var body: some Scene {
		WindowGroup {
			TaskListView(
				store: Store(
					initial: TaskListState(tasks: [
						TaskModel(title: "Task 1"),
						TaskModel(title: "Task 2")
					]),
					reducer: taskListReducer,
					middleware: [
						AnyMiddleware(TaskListValidation()),
						AnyMiddleware(TaskListEnhancement())
					]
				)
			)
		}
	}
}

Уровень сложности Store 🧠 🧠 🧠 🧠

Ну и последнее усложнение, и мы наконец-то приступим к сохранению данных в базу. Потоки. Да, этот шаг можно и опустить, но очень уж хочется дойти до более или менее полноценной архитектуры, простите...

Новый Store:

class Store<State, Action>: ObservableObject where State: Equatable {
	@Published private(set) var state: State

	private let reducer: (inout State, Action) -> Void
	private var observers: Set<Observer<State>> = []
	private var middleware: [AnyMiddleware<State, Action>]
	private let queue = DispatchQueue(label: "Store queue", qos: .userInitiated)

	init(
		initial state: State,
		reducer: @escaping (inout State, Action) -> Void,
		middleware: [AnyMiddleware<State, Action>] = []
	) {
		self.reducer = reducer
		self.state = state
		self.middleware = middleware
	}

	func dispatch(_ action: Action) {
		queue.sync {
			let middlewareChain = self.middleware.reversed().reduce({ action in
				self.reducer(&self.state, action)
				self.notifyObservers()
			}) { next, mw in
				return { action in
					mw.process(action: action, state: self.state, next: next)
				}
			}
			middlewareChain(action)
		}
	}

	func subscribe(observer: Observer<State>) {
		queue.sync {
			self.observers.insert(observer)
			self.notify(observer)
		}
	}

	private func notifyObservers() {
		for observer in observers {
			notify(observer)
		}
	}

	private func notify(_ observer: Observer<State>) {
		let state = self.state
		observer.queue.async {
			if observer.observe(state) == .dead {
				self.queue.async {
					self.observers.remove(observer)
				}
			}
		}
	}
}

Обновим Observer

final class Observer<State> {
	let queue: DispatchQueue
	let observe: (State) -> ObserverStatus

	init(queue: DispatchQueue = .main,
		 observe: @escaping (State) -> ObserverStatus) {
		self.queue = queue
		self.observe = observe
	}
}

Что нового:

  • Синхронизация операций

  • Асинхронное уведомление наблюдателей

Немного SwiftData

Middleware для работы со SwiftData:

struct TaskListPersistence: Middleware {
	private var context: ModelContext

	init(context: ModelContext) {
		self.context = context
	}

	func process(action: TaskListAction, state: TaskListState, next: @escaping (TaskListAction) -> Void) {
		switch action {
		case .addTask(let newTask):
			context.insert(newTask)
			try? context.save()
			next(.tasksLoaded(loadTasksFromDB()))
		case .removeTask(let id):
			if let taskToRemove = state.tasks.first(where: { $0.id == id }) {
				context.delete(taskToRemove)
				try? context.save()
				next(.tasksLoaded(loadTasksFromDB()))
			}
		case .loadTasks:
			let tasks = loadTasksFromDB()
			next(.tasksLoaded(tasks))
		default:
			next(action)
		}
	}

	private func loadTasksFromDB() -> [TaskModel] {
		do {
			let fetchDescriptor = FetchDescriptor<TaskModel>()
			return try context.fetch(fetchDescriptor)
		} catch {
			print("Ошибка загрузки задач: (error)")
			return []
		}
	}
}

Обновим TaskModel, чтобы таски можно было сохранить в базу данных:

@Model
class TaskModel: Equatable, Identifiable {
	@Attribute(.unique) var id = UUID()
	var title: String
	var subtitle: String?
	var isCompleted = false

	init(id: UUID = UUID(), title: String, subtitle: String? = nil, isCompleted: Bool = false) {
		self.id = id
		self.title = title
		self.subtitle = subtitle
		self.isCompleted = isCompleted
	}
}

И соберем это в main:

@main
struct TasksAlarmApp: App {

	let container: ModelContainer
	let store: Store<TaskListState, TaskListAction>

	init() {
		self.container = try! ModelContainer(for: TaskModel.self)
		self.store = Store(
			initial: TaskListState(),
			reducer: taskListReducer,
			middleware: [
				AnyMiddleware(TaskListValidation()),
				AnyMiddleware(TaskListEnhancement()),
				AnyMiddleware(TaskListPersistence(context: container.mainContext))
			]
		)
		self.store.dispatch(.loadTasks)
	}

	var body: some Scene {
		WindowGroup {
			TaskListView(store: store)
		}
	}
}
Остальные изменения помелочи
enum TaskListAction {
	case addTask(TaskModel)
	case toggleTaskCompletion(UUID)
	case removeTask(UUID)
	case filterTasks(TaskListFilter)
	case loadTasks
	case tasksLoaded([TaskModel])
}
func taskListReducer(_ state: inout TaskListState, action: TaskListAction) {
	switch action {
	case .addTask(let task):
		state.tasks.append(task)
	case .toggleTaskCompletion(let id):
		if let index = state.tasks.firstIndex(where: { $0.id == id }) {
			state.tasks[index].isCompleted.toggle()
		}
	case .removeTask(let id):
		state.tasks.removeAll { $0.id == id }
	case .filterTasks(let filter):
		state.filter = filter
	case .tasksLoaded(let tasks):
		state.tasks = tasks
		
	// Это действие обрабатывается мидлваром
	case .loadTasks:
		break
	}
}
struct TaskListView: View {
	@ObservedObject var store: Store<TaskListState, TaskListAction>

	@State private var showSheet = false
	@State private var newTaskTitle = ""
	@State private var newTaskSubtitle = ""

	var body: some View {
		NavigationView {
			ZStack {
				TaskList(store: store)

				AddTaskButton(showSheet: $showSheet)
			}
			.navigationTitle("Tasks")
			.sheet(isPresented: $showSheet) {
				NewTaskForm(
					showSheet: $showSheet,
					newTaskTitle: $newTaskTitle,
					newTaskSubtitle: $newTaskSubtitle,
					store: store
				)
			}
		}
	}
}

struct TaskList: View {
	@ObservedObject var store: Store<TaskListState, TaskListAction>

	var body: some View {
		List {
			ForEach(store.state.filteredTasks) { task in
				Text(task.title)
			}
			.onDelete { indexSet in
				indexSet.map { store.state.tasks[$0].id }
					.forEach { id in
						store.dispatch(.removeTask(id))
					}
			}
		}
		.onAppear {
			store.subscribe(observer: Observer { newState in
				print("Состояние изменилось: (newState.tasks)")
				return .alive
			})
		}
	}
}

struct AddTaskButton: View {
	@Binding var showSheet: Bool

	var body: some View {
		VStack {
			Spacer()
			HStack {
				Spacer()
				Button(action: {
					showSheet = true
				}) {
					Image(systemName: "plus")
						.foregroundColor(.white)
						.font(.system(size: 24))
						.padding()
						.background(Color.black)
						.clipShape(Circle())
						.shadow(radius: 10)
				}
				.padding()
			}
		}
	}
}

struct NewTaskForm: View {
	@Binding var showSheet: Bool
	@Binding var newTaskTitle: String
	@Binding var newTaskSubtitle: String

	var store: Store<TaskListState, TaskListAction>

	var body: some View {
		VStack {
			Text("Новая задача")
				.font(.headline)
				.padding()

			TextField("Название задачи", text: $newTaskTitle)
				.textFieldStyle(RoundedBorderTextFieldStyle())
				.padding()

			TextField("Подзаголовок задачи", text: $newTaskSubtitle)
				.textFieldStyle(RoundedBorderTextFieldStyle())
				.padding()

			HStack {
				Button("Отмена") {
					showSheet = false
				}
				.padding()

				Spacer()

				Button("Добавить") {
					let newTask = TaskModel(title: newTaskTitle, subtitle: newTaskSubtitle)
					store.dispatch(.addTask(newTask))
					// Очистка полей
					newTaskTitle = ""
					newTaskSubtitle = ""
					showSheet = false
				}
				.padding()
			}
			.padding()
		}
		.padding()
	}
}

Наш проект разросся и уже умеет добавлять, удалять и даже сохранять задачи. Мы сделали первый, но самый сложный шаг на пути к Flux.

Есть еще много других полезных структур для улучшения архитектуры проекта, они отлично ложаться на Flux, а может и произошли вместе с ним, в любом случае это тема для другой статьи


Лирическое заключение

Это моя первая в жизни статья! Я рада, что мне удалось ее дописать! Не судите строго, пожалуйста 🥹 Буду рада вашим вопросам 🙃 Конструктивная критика приветствуется 🤗

Автор: skarry

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js