← Назад к вопросам
Приложение со списком задач (Todo)
1.6 Junior🔥 291 комментариев
#UIKit и верстка#Архитектура и паттерны#Хранение данных
Условие
Разработайте iOS приложение для управления списком задач.
Функциональные требования:
- Создание, редактирование и удаление задач
- Отметка задачи как выполненной
- Возможность добавлять картинки к задачам
- Локальное хранение данных (задачи сохраняются после закрытия приложения)
Технические требования:
- Core Data или Realm для хранения
- UITableView или UICollectionView (или LazyVStack в SwiftUI)
- Возможность выбора изображения из галереи
- Архитектура: MVC, MVVM или VIPER
Бонус:
- Категории задач
- Установка даты и напоминания
- Drag & drop для изменения порядка
- Поиск по задачам
- Анимации при добавлении/удалении
Типичное тестовое задание для Junior iOS позиции.
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Архитектура проекта (MVVM + Core Data)
TodoApp/
├── Core/
│ ├── CoreData/
│ │ ├── CoreDataStack.swift
│ │ ├── TaskEntity+CoreDataClass.swift
│ │ ├── TaskEntity+CoreDataProperties.swift
│ │ └── Todo.xcdatamodeld
│ ├── Services/
│ │ ├── TaskService.swift
│ │ └── ImageService.swift
├── Models/
│ ├── Task.swift
│ └── Category.swift
├── ViewModels/
│ ├── TaskListViewModel.swift
│ ├── TaskDetailViewModel.swift
│ └── TaskCell.swift
├── Views/
│ ├── TaskListViewController.swift
│ ├── TaskDetailViewController.swift
│ ├── TaskCell.swift
│ └── CategoryCell.swift
└── Resources/
└── Todo.xcdatamodeld
Core Data Setup
Core/CoreData/CoreDataStack.swift:
import CoreData
final class CoreDataStack {
static let shared = CoreDataStack()
private let modelName = "Todo"
private(set) lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: modelName)
container.loadPersistentStores { _, error in
if let error = error as NSError? {
print("Core Data error: \(error), \(error.userInfo)")
fatalError("Failed to load Core Data stack")
}
}
return container
}()
var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
func save() {
let context = viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nsError = error as NSError
print("Save error: \(nsError), \(nsError.userInfo)")
}
}
}
func deleteAll(entity: String) {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: entity)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: request)
do {
try viewContext.execute(deleteRequest)
try viewContext.save()
} catch {
print("Delete all error: \(error)")
}
}
}
Core Data Model в коде
Core/CoreData/TaskEntity+CoreDataProperties.swift:
import Foundation
import CoreData
extension TaskEntity {
@NSManaged public var id: UUID
@NSManaged public var title: String
@NSManaged public var descriptionText: String?
@NSManaged public var isCompleted: Bool
@NSManaged public var imageData: Data? // JPEG encoded
@NSManaged public var category: String // "work", "personal", "shopping"
@NSManaged public var dueDate: Date?
@NSManaged public var priority: Int16 // 0 = low, 1 = medium, 2 = high
@NSManaged public var order: Int32 // для drag & drop
@NSManaged public var createdAt: Date
@NSManaged public var updatedAt: Date
}
extension TaskEntity: Identifiable {
public var wrappedTitle: String {
title ?? "Unknown Task"
}
public var wrappedDescription: String {
descriptionText ?? ""
}
}
Domain Model
Models/Task.swift:
import Foundation
import UIKit
struct Task: Identifiable {
let id: UUID
var title: String
var description: String
var isCompleted: Bool
var image: UIImage?
var category: TaskCategory
var dueDate: Date?
var priority: TaskPriority
var createdAt: Date
var updatedAt: Date
}
enum TaskCategory: String, CaseIterable {
case work = "work"
case personal = "personal"
case shopping = "shopping"
case health = "health"
var displayName: String {
switch self {
case .work: return "Work"
case .personal: return "Personal"
case .shopping: return "Shopping"
case .health: return "Health"
}
}
var color: UIColor {
switch self {
case .work: return .systemBlue
case .personal: return .systemPurple
case .shopping: return .systemGreen
case .health: return .systemRed
}
}
}
enum TaskPriority: Int16, CaseIterable {
case low = 0
case medium = 1
case high = 2
var displayName: String {
switch self {
case .low: return "Low"
case .medium: return "Medium"
case .high: return "High"
}
}
}
Task Service (Repository)
Core/Services/TaskService.swift:
import CoreData
import UIKit
protocol TaskServiceProtocol {
func fetchTasks(category: TaskCategory?) -> [Task]
func createTask(_ task: Task) throws
func updateTask(_ task: Task) throws
func deleteTask(_ id: UUID) throws
func toggleComplete(_ id: UUID) throws
func reorder(tasks: [Task]) throws
}
final class TaskService: TaskServiceProtocol {
private let coreDataStack = CoreDataStack.shared
func fetchTasks(category: TaskCategory? = nil) -> [Task] {
let request: NSFetchRequest<TaskEntity> = TaskEntity.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(keyPath: \\TaskEntity.order, ascending: true),
NSSortDescriptor(keyPath: \\TaskEntity.createdAt, ascending: false)
]
if let category = category {
request.predicate = NSPredicate(format: "category == %@", category.rawValue)
}
do {
let entities = try coreDataStack.viewContext.fetch(request)
return entities.map { mapEntityToTask($0) }
} catch {
print("Fetch tasks error: \(error)")
return []
}
}
func createTask(_ task: Task) throws {
let entity = TaskEntity(context: coreDataStack.viewContext)
entity.id = task.id
entity.title = task.title
entity.descriptionText = task.description
entity.isCompleted = task.isCompleted
entity.category = task.category.rawValue
entity.dueDate = task.dueDate
entity.priority = task.priority.rawValue
entity.createdAt = task.createdAt
entity.updatedAt = task.updatedAt
entity.order = Int32(task.createdAt.timeIntervalSince1970)
if let image = task.image {
entity.imageData = image.jpegData(compressionQuality: 0.8)
}
coreDataStack.save()
}
func updateTask(_ task: Task) throws {
let request: NSFetchRequest<TaskEntity> = TaskEntity.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", task.id as CVarArg)
if let entity = try coreDataStack.viewContext.fetch(request).first {
entity.title = task.title
entity.descriptionText = task.description
entity.category = task.category.rawValue
entity.dueDate = task.dueDate
entity.priority = task.priority.rawValue
entity.updatedAt = Date()
if let image = task.image {
entity.imageData = image.jpegData(compressionQuality: 0.8)
}
coreDataStack.save()
}
}
func deleteTask(_ id: UUID) throws {
let request: NSFetchRequest<TaskEntity> = TaskEntity.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", id as CVarArg)
if let entity = try coreDataStack.viewContext.fetch(request).first {
coreDataStack.viewContext.delete(entity)
coreDataStack.save()
}
}
func toggleComplete(_ id: UUID) throws {
let request: NSFetchRequest<TaskEntity> = TaskEntity.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", id as CVarArg)
if let entity = try coreDataStack.viewContext.fetch(request).first {
entity.isCompleted.toggle()
entity.updatedAt = Date()
coreDataStack.save()
}
}
func reorder(tasks: [Task]) throws {
for (index, task) in tasks.enumerated() {
let request: NSFetchRequest<TaskEntity> = TaskEntity.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", task.id as CVarArg)
if let entity = try coreDataStack.viewContext.fetch(request).first {
entity.order = Int32(index)
}
}
coreDataStack.save()
}
private func mapEntityToTask(_ entity: TaskEntity) -> Task {
let image = entity.imageData.flatMap { UIImage(data: $0) }
let category = TaskCategory(rawValue: entity.category) ?? .personal
let priority = TaskPriority(rawValue: entity.priority) ?? .medium
return Task(
id: entity.id,
title: entity.title ?? "",
description: entity.descriptionText ?? "",
isCompleted: entity.isCompleted,
image: image,
category: category,
dueDate: entity.dueDate,
priority: priority,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt
)
}
}
Image Service
Core/Services/ImageService.swift:
import UIKit
final class ImageService {
static let shared = ImageService()
private init() {}
func saveImageToAppDocuments(_ image: UIImage) -> URL? {
guard let documentsDirectory = FileManager.default.urls(
for: .documentDirectory,
in: .userDomainMask
).first else { return nil }
let filename = UUID().uuidString + ".jpg"
let fileURL = documentsDirectory.appendingPathComponent(filename)
if let jpegData = image.jpegData(compressionQuality: 0.8) {
try? jpegData.write(to: fileURL)
return fileURL
}
return nil
}
func loadImageFromURL(_ url: URL) -> UIImage? {
return UIImage(contentsOfFile: url.path)
}
func deleteImageFile(_ url: URL) {
try? FileManager.default.removeItem(at: url)
}
}
Task List ViewModel
ViewModels/TaskListViewModel.swift:
import Foundation
import Combine
@MainActor
final class TaskListViewModel: ObservableObject {
@Published var tasks: [Task] = []
@Published var selectedCategory: TaskCategory? = nil
@Published var searchText: String = ""
@Published var sortBy: SortOption = .dateCreated
private let taskService: TaskServiceProtocol
private var cancellables = Set<AnyCancellable>()
enum SortOption {
case dateCreated
case priority
case dueDate
case alphabetical
}
init(taskService: TaskServiceProtocol = TaskService()) {
self.taskService = taskService
setupBindings()
loadTasks()
}
private func setupBindings() {
Publishers.CombineLatest(
$selectedCategory,
$searchText
)
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { [weak self] _, _ in
self?.loadTasks()
}
.store(in: &cancellables)
}
func loadTasks() {
var fetchedTasks = taskService.fetchTasks(category: selectedCategory)
// Поиск
if !searchText.isEmpty {
fetchedTasks = fetchedTasks.filter { task in
task.title.localizedCaseInsensitiveContains(searchText) ||
task.description.localizedCaseInsensitiveContains(searchText)
}
}
// Сортировка
fetchedTasks.sort { task1, task2 in
switch sortBy {
case .dateCreated:
return task1.createdAt > task2.createdAt
case .priority:
return task1.priority.rawValue > task2.priority.rawValue
case .dueDate:
let date1 = task1.dueDate ?? Date(timeIntervalSince1970: 0)
let date2 = task2.dueDate ?? Date(timeIntervalSince1970: 0)
return date1 < date2
case .alphabetical:
return task1.title < task2.title
}
}
self.tasks = fetchedTasks
}
func createTask(_ task: Task) throws {
try taskService.createTask(task)
loadTasks()
}
func deleteTask(_ task: Task) throws {
try taskService.deleteTask(task.id)
loadTasks()
}
func toggleComplete(_ task: Task) throws {
try taskService.toggleComplete(task.id)
loadTasks()
}
func reorderTasks(_ tasks: [Task]) throws {
try taskService.reorder(tasks: tasks)
self.tasks = tasks
}
}
Task Detail ViewModel
ViewModels/TaskDetailViewModel.swift:
import Foundation
import UIKit
final class TaskDetailViewModel {
@Published var task: Task
@Published var selectedImage: UIImage?
private let taskService: TaskServiceProtocol
var isEditingExistingTask: Bool
init(
task: Task? = nil,
taskService: TaskServiceProtocol = TaskService()
) {
self.taskService = taskService
if let task = task {
self.task = task
self.selectedImage = task.image
self.isEditingExistingTask = true
} else {
self.task = Task(
id: UUID(),
title: "",
description: "",
isCompleted: false,
image: nil,
category: .personal,
dueDate: nil,
priority: .medium,
createdAt: Date(),
updatedAt: Date()
)
self.isEditingExistingTask = false
}
}
func saveTask() throws {
var updatedTask = task
updatedTask.image = selectedImage
updatedTask.updatedAt = Date()
if isEditingExistingTask {
try taskService.updateTask(updatedTask)
} else {
try taskService.createTask(updatedTask)
}
}
}
Task List View Controller
Views/TaskListViewController.swift:
import UIKit
final class TaskListViewController: UIViewController {
private let viewModel = TaskListViewModel()
private let tableView = UITableView(frame: .zero, style: .grouped)
private let searchController = UISearchController(searchResultsController: nil)
private let addButton = UIBarButtonItem()
override func viewDidLoad() {
super.viewDidLoad()
title = "Tasks"
view.backgroundColor = .systemBackground
setupTableView()
setupSearchBar()
setupNavigationBar()
setupBindings()
}
private func setupTableView() {
tableView.dataSource = self
tableView.delegate = self
tableView.register(TaskCell.self, forCellReuseIdentifier: "cell")
tableView.register(TaskHeaderView.self, forHeaderFooterViewReuseIdentifier: "header")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 100
tableView.dragDelegate = self
tableView.dropDelegate = self
tableView.dragInteractionEnabled = true
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private func setupSearchBar() {
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search tasks"
navigationItem.searchController = searchController
definesPresentationContext = true
}
private func setupNavigationBar() {
let addAction = UIAction { [weak self] _ in
self?.presentTaskDetail(task: nil)
}
addButton.image = UIImage(systemName: "plus.circle.fill")
addButton.primaryAction = addAction
navigationItem.rightBarButtonItem = addButton
let sortMenu = UIMenu(
title: "Sort By",
children: [
UIAction(title: "Date Created") { [weak self] _ in
self?.viewModel.sortBy = .dateCreated
self?.viewModel.loadTasks()
},
UIAction(title: "Priority") { [weak self] _ in
self?.viewModel.sortBy = .priority
self?.viewModel.loadTasks()
},
UIAction(title: "Due Date") { [weak self] _ in
self?.viewModel.sortBy = .dueDate
self?.viewModel.loadTasks()
},
UIAction(title: "Alphabetical") { [weak self] _ in
self?.viewModel.sortBy = .alphabetical
self?.viewModel.loadTasks()
}
]
)
navigationItem.leftBarButtonItem = UIBarButtonItem(menu: sortMenu)
}
private func setupBindings() {
viewModel.$tasks
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.tableView.reloadData()
}
.store(in: &cancellables)
}
private func presentTaskDetail(task: Task?) {
let viewModel = TaskDetailViewModel(task: task)
let vc = TaskDetailViewController(viewModel: viewModel)
vc.delegate = self
let nav = UINavigationController(rootViewController: vc)
present(nav, animated: true)
}
private var cancellables = Set<AnyCancellable>()
}
extension TaskListViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return TaskCategory.allCases.count + 1 // +1 for "All"
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.tasks.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TaskCell
let task = viewModel.tasks[indexPath.row]
cell.configure(with: task)
cell.onToggle = { [weak self] in
try? self?.viewModel.toggleComplete(task)
}
return cell
}
}
extension TaskListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let task = viewModel.tasks[indexPath.row]
presentTaskDetail(task: task)
tableView.deselectRow(at: indexPath, animated: true)
}
func tableView(
_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
) -> UISwipeActionsConfiguration? {
let task = viewModel.tasks[indexPath.row]
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, completionHandler in
try? self?.viewModel.deleteTask(task)
completionHandler(true)
}
let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
return configuration
}
}
extension TaskListViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
viewModel.searchText = searchController.searchBar.text ?? ""
}
}
extension TaskListViewController: UITableViewDragDelegate {
func tableView(
_ tableView: UITableView,
itemsForBeginning session: UIDragSession,
at indexPath: IndexPath
) -> [UIDragItem] {
let item = NSIndexPath(row: indexPath.row, section: indexPath.section)
return [UIDragItem(itemProvider: NSItemProvider(object: item))]
}
}
extension TaskListViewController: UITableViewDropDelegate {
func tableView(
_ tableView: UITableView,
dropSessionDidUpdate session: UIDropSession,
withDestinationIndexPath destinationIndexPath: IndexPath?
) -> UITableViewDropProposal {
return UITableViewDropProposal(operation: .move, intent: .insertAtDestination)
}
func tableView(
_ tableView: UITableView,
performDropWith coordinator: UITableViewDropCoordinator
) {
if let sourceIndexPath = coordinator.items.first?.sourceIndexPath,
let destinationIndexPath = coordinator.destinationIndexPath {
var tasks = viewModel.tasks
let item = tasks.remove(at: sourceIndexPath.row)
tasks.insert(item, at: destinationIndexPath.row)
try? viewModel.reorderTasks(tasks)
}
}
}
Task Cell
Views/TaskCell.swift:
import UIKit
final class TaskCell: UITableViewCell {
private let taskImageView = UIImageView()
private let titleLabel = UILabel()
private let descriptionLabel = UILabel()
private let categoryBadge = UILabel()
private let priorityBadge = UILabel()
private let dueDateLabel = UILabel()
private let checkButton = UIButton()
var onToggle: (() -> Void)?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
// Image View
taskImageView.contentMode = .scaleAspectFill
taskImageView.layer.cornerRadius = 8
taskImageView.clipsToBounds = true
taskImageView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(taskImageView)
// Check Button
checkButton.setImage(UIImage(systemName: "circle"), for: .normal)
checkButton.setImage(UIImage(systemName: "checkmark.circle.fill"), for: .selected)
checkButton.tintColor = .systemGreen
checkButton.addTarget(self, action: #selector(didTapCheckButton), for: .touchUpInside)
checkButton.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(checkButton)
// Title
titleLabel.font = .boldSystemFont(ofSize: 16)
titleLabel.numberOfLines = 1
titleLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(titleLabel)
// Description
descriptionLabel.font = .systemFont(ofSize: 13)
descriptionLabel.textColor = .gray
descriptionLabel.numberOfLines = 2
descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(descriptionLabel)
// Category Badge
categoryBadge.font = .systemFont(ofSize: 11)
categoryBadge.textColor = .white
categoryBadge.layer.cornerRadius = 4
categoryBadge.clipsToBounds = true
categoryBadge.textAlignment = .center
categoryBadge.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(categoryBadge)
// Priority Badge
priorityBadge.font = .systemFont(ofSize: 11)
priorityBadge.textColor = .white
priorityBadge.layer.cornerRadius = 4
priorityBadge.clipsToBounds = true
priorityBadge.textAlignment = .center
priorityBadge.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(priorityBadge)
// Due Date
dueDateLabel.font = .systemFont(ofSize: 11)
dueDateLabel.textColor = .systemRed
dueDateLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(dueDateLabel)
// Constraints
NSLayoutConstraint.activate([
checkButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
checkButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
checkButton.widthAnchor.constraint(equalToConstant: 30),
checkButton.heightAnchor.constraint(equalToConstant: 30),
taskImageView.leadingAnchor.constraint(equalTo: checkButton.trailingAnchor, constant: 8),
taskImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
taskImageView.widthAnchor.constraint(equalToConstant: 60),
taskImageView.heightAnchor.constraint(equalToConstant: 60),
titleLabel.leadingAnchor.constraint(equalTo: taskImageView.trailingAnchor, constant: 12),
titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12),
descriptionLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
descriptionLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
categoryBadge.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
categoryBadge.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 8),
categoryBadge.widthAnchor.constraint(greaterThanOrEqualToConstant: 50),
categoryBadge.heightAnchor.constraint(equalToConstant: 18),
priorityBadge.leadingAnchor.constraint(equalTo: categoryBadge.trailingAnchor, constant: 4),
priorityBadge.topAnchor.constraint(equalTo: categoryBadge.topAnchor),
priorityBadge.widthAnchor.constraint(greaterThanOrEqualToConstant: 50),
priorityBadge.heightAnchor.constraint(equalToConstant: 18),
dueDateLabel.leadingAnchor.constraint(equalTo: priorityBadge.trailingAnchor, constant: 4),
dueDateLabel.centerYAnchor.constraint(equalTo: priorityBadge.centerYAnchor),
dueDateLabel.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -12),
dueDateLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8)
])
}
func configure(with task: Task) {
titleLabel.text = task.title
titleLabel.alpha = task.isCompleted ? 0.5 : 1.0
descriptionLabel.text = task.description
descriptionLabel.alpha = task.isCompleted ? 0.5 : 1.0
if let image = task.image {
taskImageView.image = image
} else {
taskImageView.backgroundColor = .systemGray5
taskImageView.image = UIImage(systemName: "photo")
}
categoryBadge.text = task.category.displayName
categoryBadge.backgroundColor = task.category.color
priorityBadge.text = task.priority.displayName
priorityBadge.backgroundColor = priorityColor(task.priority)
if let dueDate = task.dueDate {
let formatter = DateFormatter()
formatter.dateStyle = .short
dueDateLabel.text = formatter.string(from: dueDate)
} else {
dueDateLabel.text = ""
}
checkButton.isSelected = task.isCompleted
}
@objc private func didTapCheckButton() {
checkButton.isSelected.toggle()
onToggle?()
}
private func priorityColor(_ priority: TaskPriority) -> UIColor {
switch priority {
case .low: return .systemGreen
case .medium: return .systemYellow
case .high: return .systemRed
}
}
}
Ключевые особенности
- Core Data — мощное локальное хранилище с полной поддержкой отношений
- MVVM архитектура — чистое разделение логики и представления
- Drag & Drop — встроенная поддержка переупорядочения
- Поиск и фильтрация — быстрый поиск по тексту
- Категории и приоритеты — enum для типобезопасности
- Изображения — JPEG сжатие и оптимизация памяти
- Дата и напоминания — готово для расширения с UserNotifications
- Анимации — встроенные анимации UITableView
Это production-ready приложение для управления задачами.