Skip to content

Commit

Permalink
add zoomable image details controller
Browse files Browse the repository at this point in the history
  • Loading branch information
alexdmotoc committed Aug 31, 2023
1 parent 8f8fbc6 commit 5c5d9d9
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,10 @@ class PlaceDetailsViewController: UIViewController {

extension PlaceDetailsViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("didSelect \(indexPath)")
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
let viewController = ZoomableImageViewController(photo: item, photoFetcher: viewModel.photoFetcher)
let nav = UINavigationController(rootViewController: viewController)
present(nav, animated: true)
}
}

Expand Down
20 changes: 20 additions & 0 deletions WeatherApp-iOS/UIKit/Utils/UIView+Utils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// UIView+Utils.swift
// WeatherApp-iOS
//
// Created by Alex Motoc on 30.08.2023.
//

import UIKit

extension UIView {
func pin(subview: UIView) {
addSubview(subview)
NSLayoutConstraint.activate([
subview.leadingAnchor.constraint(equalTo: leadingAnchor),
subview.topAnchor.constraint(equalTo: topAnchor),
trailingAnchor.constraint(equalTo: subview.trailingAnchor),
bottomAnchor.constraint(equalTo: subview.bottomAnchor)
])
}
}
153 changes: 153 additions & 0 deletions WeatherApp-iOS/UIKit/ZoomableImage/ZoomableImageViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//
// ZoomableImageViewController.swift
// WeatherApp-iOS
//
// Created by Alex Motoc on 30.08.2023.
//

import UIKit
import WeatherApp

class ZoomableImageViewController: UIViewController {

// MARK: - Private properties

private let photo: PlaceDetailsViewModel.Item
private let photoFetcher: PlacePhotoFetcher

// MARK: - UI

private var imageViewBottomConstraint: NSLayoutConstraint!
private var imageViewLeadingConstraint: NSLayoutConstraint!
private var imageViewTopConstraint: NSLayoutConstraint!
private var imageViewTrailingConstraint: NSLayoutConstraint!

private lazy var activityIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView()
indicator.translatesAutoresizingMaskIntoConstraints = false
indicator.hidesWhenStopped = true
indicator.startAnimating()
indicator.color = .systemGray
return indicator
}()

private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
return imageView
}()

private lazy var scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
scroll.delegate = self
scroll.showsVerticalScrollIndicator = false
scroll.showsHorizontalScrollIndicator = false
scroll.addSubview(imageView)
imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: scroll.topAnchor)
imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: scroll.leadingAnchor)
imageViewBottomConstraint = scroll.bottomAnchor.constraint(equalTo: imageView.bottomAnchor)
imageViewTrailingConstraint = scroll.trailingAnchor.constraint(equalTo: imageView.trailingAnchor)
NSLayoutConstraint.activate([
imageViewTopConstraint,
imageViewLeadingConstraint,
imageViewBottomConstraint,
imageViewTrailingConstraint
])
return scroll
}()

// MARK: - Lifecycle

init(
photo: PlaceDetailsViewModel.Item,
photoFetcher: PlacePhotoFetcher
) {
self.photo = photo
self.photoFetcher = photoFetcher
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGroupedBackground
configureViewHierarchy()
loadPhoto()
}

// MARK: - Private methods

private func configureViewHierarchy() {
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
view.pin(subview: scrollView)

let action = UIAction { [weak self] _ in self?.dismiss(animated: true) }
let item = UIBarButtonItem(systemItem: .close, primaryAction: action)
navigationItem.leftBarButtonItem = item
}

private func loadPhoto() {
Task {
do {
let data = try await photoFetcher.fetchPhoto(
reference: photo.reference,
maxWidth: photo.width,
maxHeight: photo.height
)
DispatchQueue.main.async {
self.handleDidLoadImageData(data)
}
} catch {
displayError(error)
}
}
}

private func handleDidLoadImageData(_ data: Data) {
guard let image = UIImage(data: data) else { return }
imageView.image = image
updateZoomScaleForSize(scrollView.bounds.size, image: image)
activityIndicator.stopAnimating()
view.layoutIfNeeded()
updateConstraintsForSize(scrollView.bounds.size)
}

private func updateZoomScaleForSize(_ size: CGSize, image: UIImage) {
let widthScale = size.width / image.size.width
let heightScale = size.height / image.size.height
let minScale = min(widthScale, heightScale)
scrollView.minimumZoomScale = minScale
scrollView.zoomScale = minScale
}
}

// MARK: - UIScrollViewDelegate

extension ZoomableImageViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
imageView
}

func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateConstraintsForSize(scrollView.bounds.size)
}

private func updateConstraintsForSize(_ size: CGSize) {
let yOffset = max(0, (size.height - imageView.frame.size.height) / 2)
imageViewTopConstraint.constant = yOffset
imageViewBottomConstraint.constant = yOffset

let xOffset = max(0, (size.width - imageView.frame.size.width) / 2)
imageViewLeadingConstraint.constant = xOffset
imageViewTrailingConstraint.constant = xOffset
}
}
16 changes: 16 additions & 0 deletions WeatherApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
8756CABD2A9E50F8008F99C0 /* PlaceDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8756CABC2A9E50F8008F99C0 /* PlaceDetailsViewModel.swift */; };
8756CABF2A9E629B008F99C0 /* PlaceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8756CABE2A9E629B008F99C0 /* PlaceCell.swift */; };
8756CAC22A9E63DC008F99C0 /* UIViewController+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8756CAC12A9E63DC008F99C0 /* UIViewController+Utils.swift */; };
8756CAC52A9F6A5A008F99C0 /* ZoomableImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8756CAC42A9F6A5A008F99C0 /* ZoomableImageViewController.swift */; };
8756CAC72A9F6B13008F99C0 /* UIView+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8756CAC62A9F6B13008F99C0 /* UIView+Utils.swift */; };
87768C222A9652040050B4AC /* WeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87768C212A9652040050B4AC /* WeatherView.swift */; };
87768C252A9654CB0050B4AC /* WeatherInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87768C242A9654CB0050B4AC /* WeatherInfoViewModel.swift */; };
87768C282A965D8E0050B4AC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 87768C2A2A965D8E0050B4AC /* Localizable.strings */; };
Expand Down Expand Up @@ -206,6 +208,8 @@
8756CABC2A9E50F8008F99C0 /* PlaceDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceDetailsViewModel.swift; sourceTree = "<group>"; };
8756CABE2A9E629B008F99C0 /* PlaceCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceCell.swift; sourceTree = "<group>"; };
8756CAC12A9E63DC008F99C0 /* UIViewController+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Utils.swift"; sourceTree = "<group>"; };
8756CAC42A9F6A5A008F99C0 /* ZoomableImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableImageViewController.swift; sourceTree = "<group>"; };
8756CAC62A9F6B13008F99C0 /* UIView+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Utils.swift"; sourceTree = "<group>"; };
87768C212A9652040050B4AC /* WeatherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherView.swift; sourceTree = "<group>"; };
87768C242A9654CB0050B4AC /* WeatherInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherInfoViewModel.swift; sourceTree = "<group>"; };
87768C292A965D8E0050B4AC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -555,10 +559,19 @@
isa = PBXGroup;
children = (
8756CAC12A9E63DC008F99C0 /* UIViewController+Utils.swift */,
8756CAC62A9F6B13008F99C0 /* UIView+Utils.swift */,
);
path = Utils;
sourceTree = "<group>";
};
8756CAC32A9F6A3D008F99C0 /* ZoomableImage */ = {
isa = PBXGroup;
children = (
8756CAC42A9F6A5A008F99C0 /* ZoomableImageViewController.swift */,
);
path = ZoomableImage;
sourceTree = "<group>";
};
87768C232A9654BF0050B4AC /* WeatherView */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -593,6 +606,7 @@
children = (
87768C472A979FA50050B4AC /* FavouritesList */,
8756CAB92A9E50BA008F99C0 /* PlaceDetails */,
8756CAC32A9F6A3D008F99C0 /* ZoomableImage */,
8756CAC02A9E63C1008F99C0 /* Utils */,
);
path = UIKit;
Expand Down Expand Up @@ -1021,6 +1035,7 @@
buildActionMask = 2147483647;
files = (
877D49E22A95188000AA41D3 /* MockCLLocationManager.swift in Sources */,
8756CAC72A9F6B13008F99C0 /* UIView+Utils.swift in Sources */,
87768C3F2A976E460050B4AC /* MapTabViewModel.swift in Sources */,
87768C572A97EC3B0050B4AC /* WeatherInformation+Utils.swift in Sources */,
874C646B2A94F63C00D0185F /* ContentView.swift in Sources */,
Expand All @@ -1045,6 +1060,7 @@
87768C4B2A97D0030050B4AC /* FavouritesListViewController+Utils.swift in Sources */,
87768C2C2A965F350050B4AC /* AppSettings.swift in Sources */,
87768C462A9797F00050B4AC /* FavouritesListViewModel.swift in Sources */,
8756CAC52A9F6A5A008F99C0 /* ZoomableImageViewController.swift in Sources */,
87768C4F2A97D5EF0050B4AC /* FavouritesListCell.swift in Sources */,
874C64692A94F63C00D0185F /* WeatherApp_iOSApp.swift in Sources */,
87768C252A9654CB0050B4AC /* WeatherInfoViewModel.swift in Sources */,
Expand Down

0 comments on commit 5c5d9d9

Please sign in to comment.