Создание новостной ленты с Uitableview и Uicollectionview в Ios

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

Понимание UITableView и UICollectionView

Прежде чем погрузиться в реализацию, важно понять архитектурные различия и когда выбирать одно из другого. Оба вида являются частью UIKit и следуют шаблону MVC (Model-View-Controller), но их возможности компоновки и API значительно различаются.

Оригинальное название: The Workhorse for Lists

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

UICollectionView: гибкость для сложных макетов

отделяет представление данных от алгоритма компоновки через объект.) по умолчанию компоновка потока () устраивает элементы в сетке или протекающие горизонтальные/вертикальные линии, но вы можете подклассировать ее или использовать современные для многосекционных макетов с уникальным поведением (например, горизонтальная карусель с последующим сеткой. Для лент новостей, которые смешивают типы носителей — видео, изображения, карточки статей — или требуют адаптивного интервала и динамических размеров ячейки, является превосходным выбором. Его протокол источника данных зеркально , но с и . предоставляет методы для размеров ячеек, вставок и интервалов.

Создание базовой новостной ленты

Мы пройдемся по созданию минимального контроллера просмотра новостных лент для каждого компонента. Оба примера предполагают, что вы создаете одноэкранное приложение с Xcode и Swift. Для краткости мы опускаем настройку раскадровки; мы создадим просмотры программно.

Создание новостной ленты с UITableView

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

import UIKit

class NewsTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

 private let tableView = UITableView()
 private let newsItems: [String] = [
 "Breaking: Major Policy Change Announced",
 "Tech Giants Unveil AI Partnership",
 "Local Weather Warning: Heavy Rain Expected"
 ]

 override func viewDidLoad() {
 super.viewDidLoad()
 view.backgroundColor = .white
 title = "News Feed"

 setupTableView()
 }

 private func setupTableView() {
 tableView.frame = view.bounds
 tableView.dataSource = self
 tableView.delegate = self
 tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
 tableView.rowHeight = 80
 view.addSubview(tableView)
 }

 // MARK: - UITableViewDataSource

 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
 return newsItems.count
 }

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
 let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
 cell.textLabel?.text = newsItems[indexPath.row]
 cell.textLabel?.numberOfLines = 0
 cell.accessoryType = .disclosureIndicator
 return cell
 }

 // MARK: - UITableViewDelegate

 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: Path) {
 tableView.deselectRow(at: indexPath, animated: true)
 // Navigate to article detail (to be implemented)
 }
}

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

Создание новостной ленты с помощью UICollectionView

Для ленты в стиле сетки (обычно с видео-картами с тяжелыми новостями) мы реализуем с использованием . Ниже приведен автономный пример, который отображает набор изображений с заполнителем:

import UIKit

class NewsCollectionViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

 private var collectionView: UICollectionView!
 private let imageNames = ["news1", "news2", "news3", "news4"] // Assume images in asset catalog

 override func viewDidLoad() {
 super.viewDidLoad()
 title = "Image Feed"
 setupCollectionView()
 }

 private func setupCollectionView() {
 let layout = UICollectionViewFlowLayout()
 layout.scrollDirection = .vertical
 layout.minimumInteritemSpacing = 8
 layout.minimumLineSpacing = 8
 layout.sectionInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)

 collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
 collectionView.backgroundColor = .white
 collectionView.dataSource = self
 collectionView.delegate = self
 collectionView.register(ImageCell.self, forCellWithReuseIdentifier: "ImageCell")
 view.addSubview(collectionView)
 }

 // MARK: - UICollectionViewDataSource

 func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
 return imageNames.count
 }

 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as! ImageCell
 cell.loadImage(named: imageNames[indexPath.item])
 return cell
 }

 // MARK: - UICollectionViewDelegateFlowLayout

 func collectionView(_ collectionView: UICollectionView,
 layout collectionViewLayout: UICollectionViewLayout,
 sizeForItemAt indexPath: IndexPath) -> CGSize {
 let padding: CGFloat = 8 * 3 // 3 gaps: left, interitem, right
 let availableWidth = view.frame.width - padding
 let widthPerItem = availableWidth / 2
 return CGSize(width: widthPerItem, height: widthPerItem * 1.2)
 }
}

// Custom cell class
class ImageCell: UICollectionViewCell {
 private let imageView = UIImageView()

 override init(frame: CGRect) {
 super.init(frame: frame)
 imageView.contentMode = .scaleAspectFill
 imageView.clipsToBounds = true
 contentView.addSubview(imageView)
 imageView.translatesAutoresizingMaskIntoConstraints = false
 NSLayoutConstraint.activate([
 imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
 imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
 imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
 imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
 ])
 }

 required init?(coder: NSCoder) { fatalError("init(coder:)") }

 func loadImage(named: String) {
 imageView.image = UIImage(named: named)
 }
}

Этот код создает двухколонную сетку с соотношением сторон 1,2:1 на ячейку. Вы можете настроить на для подачи в стиле карусели. Для реальных приложений замените изображения заполнителей с асинхронной загрузкой изображения через или библиотеки, такие как Kingfisher .

Настройка клеток для более богатой новостной ленты

Простой текстовой ячейки недостаточно для современного новостного приложения. Вам нужно отображать миниатюры, даты публикации, имена источников, а иногда и интерактивные кнопки (поделиться, сохранить). Это требует создания пользовательских подклассов или с правильно выложенными подпросмотрами.

Создание ячейки с характерной статьей (UITableView)

Создайте ячейку, которая включает в себя для большого изображения героя, для заголовка, еще одну для выдержки и небольшую этикетку для метки времени. Используйте автоматограф, чтобы убедиться, что ячейка расширяется правильно. Пример:

class ArticleCell: UITableViewCell {
 let heroImageView = UIImageView()
 let titleLabel = UILabel()
 let excerptLabel = UILabel()
 let dateLabel = UILabel()

 override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
 super.init(style: style, reuseIdentifier: reuseIdentifier)
 setupViews()
 }

 required init?(coder: NSCoder) { fatalError() }

 private func setupViews() {
 heroImageView.contentMode = .scaleAspectFill
 heroImageView.clipsToBounds = true
 titleLabel.font = UIFont.boldSystemFont(ofSize: 18)
 titleLabel.numberOfLines = 2
 excerptLabel.font = UIFont.systemFont(ofSize: 14)
 excerptLabel.textColor = .gray
 excerptLabel.numberOfLines = 3
 dateLabel.font = UIFont.systemFont(ofSize: 12)
 dateLabel.textColor = .lightGray

 for subview in [heroImageView, titleLabel, excerptLabel, dateLabel] {
 subview.translatesAutoresizingMaskIntoConstraints = false
 contentView.addSubview(subview)
 }

 // Constraints – simplified; in practice use stack views or manual anchors
 NSLayoutConstraint.activate([
 heroImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
 heroImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
 heroImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
 heroImageView.heightAnchor.constraint(equalToConstant: 200),

 titleLabel.topAnchor.constraint(equalTo: heroImageView.bottomAnchor, constant: 8),
 titleLabel.leadingAnchor.constraint(equalTo: heroImageView.leadingAnchor),
 titleLabel.trailingAnchor.constraint(equalTo: heroImageView.trailingAnchor),

 excerptLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
 excerptLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
 excerptLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),

 dateLabel.topAnchor.constraint(equalTo: excerptLabel.bottomAnchor, constant: 4),
 dateLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
 dateLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8)
 ])
 }

 func configure(with article: Article) {
 titleLabel.text = article.title
 excerptLabel.text = article.excerpt
 dateLabel.text = article.dateString
 // Load image asynchronously (omitted)
 }
}

В контроллере просмотра зарегистрируйте эту пользовательскую ячейку и реализуйте соответственно. Установите и , чтобы разрешить переменную высоту.

Адаптация клетки для UICollectionView

Тот же шаблон применим и к . Поскольку представления коллекции могут иметь разные макеты, вы можете спроектировать ячейку в стиле «карты», которая работает как в вертикальном, так и в горизонтальном режимах прокрутки. Используйте , чтобы упростить выравнивание. Вот компактная версия:

class NewsCardCell: UICollectionViewCell {
 private let cardView = UIView()
 private let imageView = UIImageView()
 private let titleLabel = UILabel()

 override init(frame: CGRect) {
 super.init(frame: frame)
 cardView.backgroundColor = .systemBackground
 cardView.layer.cornerRadius = 10
 cardView.clipsToBounds = true
 cardView.translatesAutoresizingMaskIntoConstraints = false
 contentView.addSubview(cardView)

 imageView.contentMode = .scaleAspectFill
 imageView.clipsToBounds = true
 imageView.translatesAutoresizingMaskIntoConstraints = false
 titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
 titleLabel.numberOfLines = 2
 titleLabel.translatesAutoresizingMaskIntoConstraints = false

 cardView.addSubview(imageView)
 cardView.addSubview(titleLabel)

 // Layout
 NSLayoutConstraint.activate([
 cardView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4),
 cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 4),
 cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4),
 cardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4),

 imageView.topAnchor.constraint(equalTo: cardView.topAnchor),
 imageView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor),
 imageView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor),
 imageView.heightAnchor.constraint(equalToConstant: 120),

 titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 8),
 titleLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 8),
 titleLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -8),
 titleLabel.bottomAnchor.constraint(lessThanOrEqualTo: cardView.bottomAnchor, constant: -8)
 ])
 }

 required init?(coder: NSCoder) { fatalError() }
}

С помощью этой ячейки, вид коллекции может отображать канал в стиле Pinterest, если вы предоставляете различные размеры предметов через делегата.

Добавление обновлений Pull-to-Refresh и Real-Time

Современные новостные ленты должны поддерживать жесты обновления и динамическую загрузку. Класс легко интегрируется как с , так и , когда просмотр прокрутки уже подключен к управлению обновлениями. В :

let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(refreshNewsFeed), for: .valueChanged)
tableView.refreshControl = refreshControl
// or collectionView.refreshControl = refreshControl

В методе , выполнить асинхронный вывод данных (например, из REST API), затем перезагрузить вид на главной очереди и вызвать . Пример:

@objc private func refreshNewsFeed() {
 NewsAPIClient.fetchLatestArticles { [weak self] articles in
 DispatchQueue.main.async {
 self?.newsItems = articles
 self?.tableView.reloadData()
 self?.tableView.refreshControl?.endRefreshing()
 }
 }
}

Для обновлений в реальном времени, таких как новости в прямом эфире, рассмотрите возможность использования или сторонней библиотеки, такой как База данных Firebase в реальном времени.

// For UITableView
tableView.performBatchUpdates({
 tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .top)
}, completion: nil)

// For UICollectionView
collectionView.performBatchUpdates({
 collectionView.insertItems(at: [IndexPath(item: 0, section: 0)])
}, completion: nil)

Обработка пользовательских взаимодействий и навигации

Когда пользователь нажимает на новостной элемент, приложение должно перейти к просмотру деталей. Внедрить метод делегата и использовать либо раскадровку, либо программный толчок. Для таблиц и коллекций метод делегата обеспечивает путь индекса. Пример для простого :

// In UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
 tableView.deselectRow(at: indexPath, animated: true)
 let article = newsItems[indexPath.row]
 let detailVC = ArticleDetailViewController(article: article)
 navigationController?.pushViewController(detailVC, animated: true)
}

// In UICollectionViewDelegate
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
 let article = newsItems[indexPath.item]
 let detailVC = ArticleDetailViewController(article: article)
 navigationController?.pushViewController(detailVC, animated: true)
}

Рассмотрите возможность добавления жестов длинного нажатия для дополнительных действий (обмена, закладки) через , прикрепленных к коллекции или просмотру таблицы, или используйте контекстные меню, введенные в iOS 13.

Соображения производительности для крупных кормов

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

Современные подходы: композиционное слое и дифференцируемый источник данных

iOS 13 и более поздние версии представили два мощных API, которые значительно упрощают сложные новостные ленты: и (которые также работают с через ).

UICollectionViewКомпозиционный макет

Эта компоновка позволяет определять секции с различным поведением — например, секция баннера героя, которая прокручивается горизонтально, сетка новостей и вертикальный список последних статей. Каждый раздел определяется , который содержит группы и элементы.

let layout = UICollectionViewCompositionalLayout { (sectionIndex, environment) -> NSCollectionLayoutSection? in
 switch sectionIndex {
 case 0:
 // Horizontal carousel
 let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
 let item = NSCollectionLayoutItem(layoutSize: itemSize)
 let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.8), heightDimension: .absolute(200))
 let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
 let section = NSCollectionLayoutSection(group: group)
 section.orthogonalScrollingBehavior = .continuous
 section.interGroupSpacing = 10
 return section
 case 1:
 // Vertical grid (2 columns)
 let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .estimated(150))
 let item = NSCollectionLayoutItem(layoutSize: itemSize)
 let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(150))
 let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
 group.interItemSpacing = .fixed(8)
 let section = NSCollectionLayoutSection(group: group)
 section.interGroupSpacing = 8
 section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)
 return section
 default:
 return nil
 }
}

collectionView.collectionViewLayout = layout

Диффабельный источник данных

Вместо ручного управления и управления индексным путем, дифференцируемые источники данных позволяют применять снимки. Это устраняет несоответствия и упрощает анимацию. Пример для :

enum Section {
 case featured
 case latest
}

struct Article: Hashable {
 let id: UUID
 let title: String
 // ...
}

var dataSource: UICollectionViewDiffableDataSource!

// In viewDidLoad:
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
 (collectionView, indexPath, article) -> UICollectionViewCell? in
 let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ArticleCell
 cell.configure(with: article)
 return cell
}

// When data arrives:
var snapshot = NSDiffableDataSourceSnapshot()
snapshot.appendSections([.featured, .latest])
snapshot.appendItems(featuredArticles, toSection: .featured)
snapshot.appendItems(latestArticles, toSection: .latest)
dataSource.apply(snapshot, animatingDifferences: true)

Диффабельные источники данных также могут использоваться с через . Эта схема теперь рекомендована Apple для всех новых проектов, поскольку она устраняет распространенные ошибки и упрощает управление состоянием.

Интеграция с реальными источниками данных

Чтобы сделать вашу новостную ленту динамичной, подключите ее к backend API. Общие подходы включают в себя:

  • REST API: Используйте , чтобы принести JSON, декодируйте его и заполните массив моделей. Пример с (iOS 15+):
func fetchArticles() async throws -> [Article] {
 let url = URL(string: "https://api.example.com/news")!
 let (data, _) = try await URLSession.shared.data(from: url)
 return try JSONDecoder().decode([Article].self, from: data)
}
  • Основные данные / CloudKit: Для автономных каналов сначала храните статьи локально с использованием основных данных, а затем синхронизируйте их с облачным бэкэндом. Используйте с для автоматического обновления пользовательского интерфейса при изменении данных.
  • Firebase Firestore: Слушатели в режиме реального времени могут напрямую обновлять снимок просмотра коллекции.

Всегда исправляйте ошибки изящно — покажите заполнитель, кнопку повторного использования или кэшированную версию корма.

Продвинутые кастомизации и польский

Чтобы сделать вашу ленту новостей выделяющейся, рассмотрите следующие улучшения:

  • Переход анимированных клеток: Используйте или метод сбора изображения , чтобы исчезнуть в клетках по мере их появления.
  • Контекстные меню и действия свайпа: На , реализовать , чтобы пометить статьи как прочитанные или сохранить их., использовать через делегата.
  • Легкие заголовки разделов: Для группированных кормов сделайте заголовки разделов «липкими», используя простой стиль просмотра таблицы или настроив на композиционную компоновку.
  • Темный режим и динамический тип: Используйте и , чтобы обеспечить соблюдение системных настроек корма.

Тестирование вашей новостной ленты

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

Заключение

Создание надежной новостной ленты в iOS требует четкого понимания и , наряду с современными инструментами, такими как композиционные макеты и дифференцируемые источники данных. Следуя шаблонам, изложенным в этом руководстве — дизайн пользовательских ячеек, асинхронная загрузка данных, перезагрузка и тщательная настройка производительности — вы можете создавать привлекательные, отзывчивые новостные ленты, которые радуют пользователей. Официальная документация Apple UITableView и UICollectionView являются бесценными ссылками, как и ресурсы сообщества, такие как . С помощью этих инструментов вы хорошо оснащены для создания любой новостной ленты, требуемой вашим приложением.