iOS Swift:如何重新创建照片应用程序 UICollectionView 布局
iOS Swift: How to recreate a Photos App UICollectionView Layout
我暂时想知道如何创建 iOS 照片应用布局。我怎样才能让它看起来像放大到一个集合,同时导航栏显示一个后退按钮?
它是被推送到 UINavigationController 上的新视图控制器吗?如果是这样,他们究竟是如何在扩展时设法匹配瓷砖的。
是否有第 3 方库可以让我轻松地重新创建这样的布局?
希望您能帮助我理解其工作原理。
创建相册可以参考:https://github.com/inspace-io/INSPhotoGallery.
这是一个很好的库,可以显示照片、缩放功能等等。
回答您的第一个问题,"Is it a new view controller which gets pushed onto a UINavigationController?"。是的,它是一个新的视图控制器。 Apple 在这里使用的是一个 UIViewControllerTransitioningDelegate,它允许您呈现关于如何呈现和关闭视图控制器的自定义动画。
关于第二个问题,"Hope you can help me to understand the concept of how this works."涉及的内容很多,不好说。我已经重新创建了我将在下面展示的效果,但首先我需要解释一些核心原则。
来自 Apple 的文档,
When implementing your transitioning delegate object, you can return different animator objects depending on whether a view controller is being presented or dismissed. All transitions use a transition animator object—an object that conforms to the UIViewControllerAnimatedTransitioning protocol—to implement the basic animations. A transition animator object performs a set of animations over a finite period of time.
换句话说,UIViewControllerTransitioningDelegate 需要一个您创建的动画对象,它描述了视图控制器应该如何呈现以及它应该如何消失。这些委托方法中只有两个与您想要实现的目标相关,它们是:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = PresentAnimator()
return animator
}
这会要求您的委托提供在呈现视图控制器时要使用的过渡动画对象。
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = DismissAnimator()
return animator
}
这会要求您的委托提供在关闭视图控制器时要使用的过渡动画对象。
PresentAnimator 和 DismissAnimator 对象都符合 UIViewControllerAnimatedTransitioning。来自 Apple 的文档:
In your animator object, implement the transitionDuration(using:) method to specify the duration of your transition and implement the animateTransition(using:) method to create the animations themselves. Information about the objects involved in the transition is passed to your animateTransition(using:) method in the form of a context object. Use the information provided by that object to move the target view controller’s view on or off screen over the specified duration.
基本上,每个动画对象都会描述视图控制器动画的持续时间以及动画的方式。
下面是所有这一切的演示。这就是我们将实现的目标:
在故事板中创建两个视图控制器。我的第一个视图控制器称为 ViewController,它包含一个集合视图和一个带有标识符 "MediaCell" 的集合视图单元格以及一个填充该集合视图单元格的图像。集合视图单元格有一个名为 ImageCollectionViewCell 的 class,只有这个:
class ImageCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var image: UIImageView! //links to the collection view cell's image
}
我的第二个视图控制器称为 ImageRevealViewController,它只有一个图像视图和顶部的灰色视图,我用它来模拟导航栏和自定义后退按钮(我已经尝试了所有这是一个普通的 UINavigationController 导航栏,但是解雇动画师无法工作。虽然我的只是为了演示,但制作看起来和行为都像导航栏的东西并没有什么可耻的。
相册
这将是您 ViewController 的代码。基本上,这将是用户找到照片集的地方,就像相册一样。如您所见,我为我使用了两个测试图像。
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
@IBOutlet weak var collectionView: UICollectionView!
var selectedCell = UICollectionViewCell() //the selected cell, important for the animator
var media: [UIImage] = [UIImage]() //the photo album's images
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
media.append(UIImage(named: "testimage1")!)
media.append(UIImage(named: "testimage2")!)
collectionView.delegate = self
collectionView.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedCell = collectionView.cellForItem(at: indexPath)!
let selectedCellImage = selectedCell as! ImageCollectionViewCell
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
let imageRevealVC = mainStoryboard.instantiateViewController(withIdentifier: "ImageRevealVC") as! ImageRevealViewController
imageRevealVC.transitioningDelegate = self
imageRevealVC.imageToReveal = selectedCellImage.image.image
/*
This is where I tried using the nav controller but things did not work out for the dismiss animator. I have commented it out.
*/
//let navController = UINavigationController(rootViewController: imageRevealVC)
//navController.transitioningDelegate = self
//navigationController?.pushViewController(imageRevealVC, animated: true)
present(imageRevealVC, animated: true, completion: nil)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return media.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MediaCell", for: indexPath) as! ImageCollectionViewCell
cell.image.image = media[indexPath.row]
cell.image.contentMode = .scaleAspectFill
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let itemsPerRow:CGFloat = 3
let hardCodedPadding:CGFloat = 2
let itemWidth = (collectionView.bounds.width / itemsPerRow) - hardCodedPadding
let itemHeight = itemWidth
return CGSize(width: itemWidth, height: itemHeight)
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = PresentAnimator()
animator.originFrame = selectedCell.frame //the selected cell gives us the frame origin for the reveal animation
return animator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = DismissAnimator()
return animator
}
}
UIViewControllerTransitioningDelegate 在我谈到的动画对象旁边。请注意,在集合视图的 didSelect 中,我实例化了新的视图控制器并使其转换委托等于 self.
动画师
制作动画师总是需要三个步骤。
- 设置转换
- 创建动画
- 完成转换
现在是现在的动画师。创建一个名为 PresentAnimator 的新 Swift class 并添加以下内容:
import Foundation
import UIKit
class PresentAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 0.5
var originFrame = CGRect.zero
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
//2) create animation
let finalFrame = toView.frame
let xScaleFactor = originFrame.width / finalFrame.width
let yScaleFactor = originFrame.height / finalFrame.height
let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor)
toView.transform = scaleTransform
toView.center = CGPoint(
x: originFrame.midX,
y: originFrame.midY
)
toView.clipsToBounds = true
containerView.addSubview(toView)
UIView.animate(withDuration: duration, delay: 0.0,
options: [], animations: {
toView.transform = CGAffineTransform.identity
toView.center = CGPoint(
x: finalFrame.midX,
y: finalFrame.midY
)
}, completion: {_ in
//3 complete the transition
transitionContext.completeTransition(
!transitionContext.transitionWasCancelled
)
})
}
}
现在是解散动画师。创建一个名为 DismissAnimator 的新 class 并添加以下内容:
import Foundation
import UIKit
class DismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 0.5
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//1) setup the transition
let containerView = transitionContext.containerView
let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
containerView.insertSubview(toView, belowSubview: fromView)
//2) animations!
UIView.animate(withDuration: duration, delay: 0.0, options: [], animations: {
fromView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
}, completion: {_ in
//3) complete the transition
transitionContext.completeTransition(
!transitionContext.transitionWasCancelled
)
})
}
}
图像显示
现在是最后一步,显示图像的视图控制器。在您的 ImageRevealController 中添加:
import UIKit
class ImageRevealViewController: UIViewController {
var imageToReveal: UIImage!
@IBOutlet weak var imageRevealed: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
imageRevealed.image = imageToReveal
}
@IBAction func backButton(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
backButton 连接到我添加到像导航栏一样的视图的按钮。您可以添加自己的后退指示器以使其更真实。
有关 UIViewControllerTransitioningDelegate 的更多信息,这里有一个部分 "From View Controller" disappears using UIViewControllerContextTransitioning,您可以查看我已经提供的答案。
我暂时想知道如何创建 iOS 照片应用布局。我怎样才能让它看起来像放大到一个集合,同时导航栏显示一个后退按钮?
它是被推送到 UINavigationController 上的新视图控制器吗?如果是这样,他们究竟是如何在扩展时设法匹配瓷砖的。
是否有第 3 方库可以让我轻松地重新创建这样的布局?
希望您能帮助我理解其工作原理。
创建相册可以参考:https://github.com/inspace-io/INSPhotoGallery.
这是一个很好的库,可以显示照片、缩放功能等等。
回答您的第一个问题,"Is it a new view controller which gets pushed onto a UINavigationController?"。是的,它是一个新的视图控制器。 Apple 在这里使用的是一个 UIViewControllerTransitioningDelegate,它允许您呈现关于如何呈现和关闭视图控制器的自定义动画。
关于第二个问题,"Hope you can help me to understand the concept of how this works."涉及的内容很多,不好说。我已经重新创建了我将在下面展示的效果,但首先我需要解释一些核心原则。
来自 Apple 的文档,
When implementing your transitioning delegate object, you can return different animator objects depending on whether a view controller is being presented or dismissed. All transitions use a transition animator object—an object that conforms to the UIViewControllerAnimatedTransitioning protocol—to implement the basic animations. A transition animator object performs a set of animations over a finite period of time.
换句话说,UIViewControllerTransitioningDelegate 需要一个您创建的动画对象,它描述了视图控制器应该如何呈现以及它应该如何消失。这些委托方法中只有两个与您想要实现的目标相关,它们是:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = PresentAnimator()
return animator
}
这会要求您的委托提供在呈现视图控制器时要使用的过渡动画对象。
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = DismissAnimator()
return animator
}
这会要求您的委托提供在关闭视图控制器时要使用的过渡动画对象。
PresentAnimator 和 DismissAnimator 对象都符合 UIViewControllerAnimatedTransitioning。来自 Apple 的文档:
In your animator object, implement the transitionDuration(using:) method to specify the duration of your transition and implement the animateTransition(using:) method to create the animations themselves. Information about the objects involved in the transition is passed to your animateTransition(using:) method in the form of a context object. Use the information provided by that object to move the target view controller’s view on or off screen over the specified duration.
基本上,每个动画对象都会描述视图控制器动画的持续时间以及动画的方式。
下面是所有这一切的演示。这就是我们将实现的目标:
在故事板中创建两个视图控制器。我的第一个视图控制器称为 ViewController,它包含一个集合视图和一个带有标识符 "MediaCell" 的集合视图单元格以及一个填充该集合视图单元格的图像。集合视图单元格有一个名为 ImageCollectionViewCell 的 class,只有这个:
class ImageCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var image: UIImageView! //links to the collection view cell's image
}
我的第二个视图控制器称为 ImageRevealViewController,它只有一个图像视图和顶部的灰色视图,我用它来模拟导航栏和自定义后退按钮(我已经尝试了所有这是一个普通的 UINavigationController 导航栏,但是解雇动画师无法工作。虽然我的只是为了演示,但制作看起来和行为都像导航栏的东西并没有什么可耻的。
相册
这将是您 ViewController 的代码。基本上,这将是用户找到照片集的地方,就像相册一样。如您所见,我为我使用了两个测试图像。
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
@IBOutlet weak var collectionView: UICollectionView!
var selectedCell = UICollectionViewCell() //the selected cell, important for the animator
var media: [UIImage] = [UIImage]() //the photo album's images
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
media.append(UIImage(named: "testimage1")!)
media.append(UIImage(named: "testimage2")!)
collectionView.delegate = self
collectionView.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedCell = collectionView.cellForItem(at: indexPath)!
let selectedCellImage = selectedCell as! ImageCollectionViewCell
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
let imageRevealVC = mainStoryboard.instantiateViewController(withIdentifier: "ImageRevealVC") as! ImageRevealViewController
imageRevealVC.transitioningDelegate = self
imageRevealVC.imageToReveal = selectedCellImage.image.image
/*
This is where I tried using the nav controller but things did not work out for the dismiss animator. I have commented it out.
*/
//let navController = UINavigationController(rootViewController: imageRevealVC)
//navController.transitioningDelegate = self
//navigationController?.pushViewController(imageRevealVC, animated: true)
present(imageRevealVC, animated: true, completion: nil)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return media.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MediaCell", for: indexPath) as! ImageCollectionViewCell
cell.image.image = media[indexPath.row]
cell.image.contentMode = .scaleAspectFill
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let itemsPerRow:CGFloat = 3
let hardCodedPadding:CGFloat = 2
let itemWidth = (collectionView.bounds.width / itemsPerRow) - hardCodedPadding
let itemHeight = itemWidth
return CGSize(width: itemWidth, height: itemHeight)
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = PresentAnimator()
animator.originFrame = selectedCell.frame //the selected cell gives us the frame origin for the reveal animation
return animator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = DismissAnimator()
return animator
}
}
UIViewControllerTransitioningDelegate 在我谈到的动画对象旁边。请注意,在集合视图的 didSelect 中,我实例化了新的视图控制器并使其转换委托等于 self.
动画师
制作动画师总是需要三个步骤。
- 设置转换
- 创建动画
- 完成转换
现在是现在的动画师。创建一个名为 PresentAnimator 的新 Swift class 并添加以下内容:
import Foundation
import UIKit
class PresentAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 0.5
var originFrame = CGRect.zero
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
//2) create animation
let finalFrame = toView.frame
let xScaleFactor = originFrame.width / finalFrame.width
let yScaleFactor = originFrame.height / finalFrame.height
let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor)
toView.transform = scaleTransform
toView.center = CGPoint(
x: originFrame.midX,
y: originFrame.midY
)
toView.clipsToBounds = true
containerView.addSubview(toView)
UIView.animate(withDuration: duration, delay: 0.0,
options: [], animations: {
toView.transform = CGAffineTransform.identity
toView.center = CGPoint(
x: finalFrame.midX,
y: finalFrame.midY
)
}, completion: {_ in
//3 complete the transition
transitionContext.completeTransition(
!transitionContext.transitionWasCancelled
)
})
}
}
现在是解散动画师。创建一个名为 DismissAnimator 的新 class 并添加以下内容:
import Foundation
import UIKit
class DismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 0.5
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//1) setup the transition
let containerView = transitionContext.containerView
let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
containerView.insertSubview(toView, belowSubview: fromView)
//2) animations!
UIView.animate(withDuration: duration, delay: 0.0, options: [], animations: {
fromView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
}, completion: {_ in
//3) complete the transition
transitionContext.completeTransition(
!transitionContext.transitionWasCancelled
)
})
}
}
图像显示
现在是最后一步,显示图像的视图控制器。在您的 ImageRevealController 中添加:
import UIKit
class ImageRevealViewController: UIViewController {
var imageToReveal: UIImage!
@IBOutlet weak var imageRevealed: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
imageRevealed.image = imageToReveal
}
@IBAction func backButton(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
backButton 连接到我添加到像导航栏一样的视图的按钮。您可以添加自己的后退指示器以使其更真实。
有关 UIViewControllerTransitioningDelegate 的更多信息,这里有一个部分 "From View Controller" disappears using UIViewControllerContextTransitioning,您可以查看我已经提供的答案。