- Use all we've learnt so far to build a photo search application using a custom compositional layout.
- Use the
Combineframework to make asynchronous network requests via aPublisherandSubscriber. - Use
UISearchControlleralong with thedebounceCombine operator to prevent multiple network requests from the search bar.
import UIKit
class ImageCell: UICollectionViewCell {
static let reuseIdentifier = "imageCell"
public lazy var imageView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(systemName: "photo")
iv.layer.cornerRadius = 8
iv.clipsToBounds = true
return iv
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
imageViewConstraints()
}
private func imageViewConstraints() {
addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
])
}
}struct Config {
static let apikey = "API KEY GOES HERE FOR THE PIXABAY API"
}struct PhotoResultsWrapper: Decodable {
let hits: [Photo]
}
struct Photo: Decodable, Hashable {
let id: Int
let webformatURL: String
}Allows any property to be a Publisher and emit values over time. e.g in this app as the user enters text into the search bar the subscriber will have access to the values the user enters. This user entered input will then be passed over to the api client to perform the photo search from Pixabay.
This is a wrapper around URLSession that Combine provides and allows us to create a Publisher that we will be subscribing on in our view controller to get the results of the photo search.
Allows a scheduled time before carrying out a specific task. In the case of our app we will add a second delay after the user finishes to type before running the network request of their search. This will prevent multiple requests from going to the Pixabay API as the user is typing.
Allows you to embed a search bar into the navigation bar item and is a more modern way to carry out searches with some more flexibility as opposed to your standard UISearchBar api. Also you can assign a specific view controller to be the results controller among other features UISearchController provides.
In this app we will have only 1 section.
enum SectionKind: Int, CaseIterable {
case main
}private var collectionView: UICollectionView!typealias DataSource = UICollectionViewDiffableDataSource<SectionKind, Photo>
private var dataSource: DataSource!private var searchController: UISearchController!This property will be a Publisher and be able to emit values as it's changed.
@Published private var searchText: String? = "paris"Those subscriptions will be released from memory when deinit is called on the view controller.
private var subscriptions: Set<AnyCancellable> = []import Foundation
import Combine
class APIClient {
public func searchPhotos(for query: String) -> AnyPublisher<[Photo], Error> {
let query = query.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "paris"
let perPage = 200 // max is 200
let endpoint = "https://pixabay.com/api/?key=\(Config.apikey)&q=\(query)&per_page=\(perPage)&safesearch=true"
let url = URL(string: endpoint)!
// using combine for asynchronous networking
// create a publisher
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: PhotoResultsWrapper.self, decoder: JSONDecoder())
.map { $0.hits }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}private let apiClient = APIClient() In this layout with have a leading and a trailing group embedded in a container group. The leading group has 2 items vertically aligned and the trailing group has 3 items vertically aligned.
private func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let itemSpacing: CGFloat = 5
item.contentInsets = NSDirectionalEdgeInsets(top: itemSpacing, leading: itemSpacing, bottom: itemSpacing, trailing: itemSpacing)
let innerGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0))
let leadingGroup = NSCollectionLayoutGroup.vertical(layoutSize: innerGroupSize, subitem: item, count: 2)
let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: innerGroupSize, subitem: item, count: 3)
let nestedGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(1000))
let nestedGroup = NSCollectionLayoutGroup.horizontal(layoutSize: nestedGroupSize, subitems: [leadingGroup, trailingGroup])
let section = NSCollectionLayoutSection(group: nestedGroup)
return section
}
return layout
}collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
collectionView.register(ImageCell.self, forCellWithReuseIdentifier: ImageCell.reuseIdentifier)
collectionView.backgroundColor = .systemBackground
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self
view.addSubview(collectionView)Add Kingfisher dependency https://github.com/onevcat/Kingfisher via Swift Package Manager or Cocoapods for network image processing on our custom ImageCell's imageView property.
dataSource = DataSource(collectionView: collectionView, cellProvider: { (collectionView, indexPath, photo) -> UICollectionViewCell? in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImageCell.reuseIdentifier, for: indexPath) as? ImageCell else {
fatalError()
}
cell.imageView.kf.indicatorType = .activity
cell.imageView.kf.setImage(with: URL(string: photo.webformatURL))
cell.imageView.contentMode = .scaleAspectFill
cell.layer.cornerRadius = 10
return cell
})searchController = UISearchController(searchResultsController: nil)
navigationItem.searchController = searchController
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.autocapitalizationType = .noneThis method gets called everytime the user inputs into the search bar.
extension PhotoSearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text,
!text.isEmpty else {
return
}
searchText = text
}
}Using the $ before a property's instance says you are now subscribing to this Publisher property and would like to receive its values as they are emitted.
// subscribe to searchText
$searchText
.debounce(for: .seconds(1.0), scheduler: RunLoop.main)
.removeDuplicates()
.sink { (text) in
self.searchPhotos(for: text!)
}
.store(in: &subscriptions)private func searchPhotos(for query: String) {
apiClient.searchPhotos(for: query)
.removeDuplicates()
.sink(receiveCompletion: { (completion) in
print(completion)
}) { [weak self] (photos) in
self?.updateSnapshot(with: photos)
}
.store(in: &subscriptions)
}Implement scrollViewWillBeginDragging(_ :) so the keyboard is dismissed at the user drags the on the collection view up or down.
extension PhotoSearchViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("\(indexPath.section), \(indexPath.row)")
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
searchController.searchBar.resignFirstResponder()
}
}Use this endpoint https://api.tvmaze.com/shows/431/episodes from the TVMaze API to create an app that populates a collection view with all 10 seasons of Friends.
- The app should use the Combine framework to make network requests.
- The app should have 10 sections with header views using compositional layout.
- The first header view should consist of a Friends Poster image you find on the web, e.g the height of the header view for the image can be 400 points. The image should take up the entire width of the section.
- Show the summary of an episode along with the
originalimage from the TVMaze API. - Use your own creativity to make the app spark.
- Create a Search controller so the user can search for any tv show.
- When the user selects a show it will segue to your controller from the challenge above.

