En-tête extensible avec UIPageViewController
Mon problème semble évident et dédoublé mais je n'arrive pas à le faire fonctionner.
J'essaie d'obtenir le fameux effet d'en-tête extensible (le haut de l'image collé au haut UIScrollView
lors du défilement), mais avec une UIPageViewController
au lieu d'une simple image.
Ma structure est:
UINavigationBar
|-- UIScrollView
|-- UIView (totally optional container)
|-- UIPageViewController (as UIView, embedded with addChild()) <-- TO STICK
|-- UIHostingViewController (SwiftUI view with labels, also embedded)
|-- UITableView (not embedded but could be)
Ma UIPageViewController
contient des images pour faire un carrousel, rien de plus.
Toutes mes vues sont disposées avec NSLayoutConstraint
s (avec format visuel pour la disposition verticale dans le conteneur).
J'essaie de coller topAnchor
la vue du contrôleur de page à celle de self.view
(avec ou sans priority
) mais pas de chance, et quoi que je fasse, cela ne change absolument rien.
J'ai finalement essayé de l'utiliser SnapKit
mais ça ne marche pas non plus (je n'y connais pas grand chose mais ça semble n'être qu'un wrapper pour NSLayoutConstaint
s donc je ne suis pas surpris que ça ne marche pas aussi).
J'ai suivi ce tutoriel, celui-ci et celui-là mais aucun d'eux n'a fonctionné.
(How) can I achieve what I want?
EDIT 1:
Pour clarifier, mon carrousel a actuellement une hauteur forcée de 350. Je veux obtenir cet effet exact (qui est montré avec un seul UIImageView
) sur tout mon carrousel:
To clarify as much as possible, I want to replicate this effect to my whole UIPageViewController
/carousel so that the displayed page/image can have this effect when scrolled.
NOTE: comme mentionné dans la structure ci-dessus, j'ai une barre de navigation (transparente), et mes encarts de zone de sécurité sont respectés (rien ne va sous la barre d'état). Je ne pense pas que cela changerait la solution (car la solution est probablement un moyen de coller le haut du carrousel à self.view
, peu importe le cadre de self.view
) mais je préfère que vous sachiez tout.
Solution du problème
Votre hiérarchie de vues doit être :
UINavigationBar
|-- UIScrollView
|-- UIView ("stretchy" container view)
|-- UIPageViewController (as UIView, embedded with asChild())
|-- UIHostingViewController (SwiftUI view with labels, also embedded)
Pour obtenir la vue extensible pour "coller au sommet":
Nous contraignons le haut de la vue extensible au haut de la vue de défilement .frameLayoutGuide
, mais nous donnons à cette contrainte une valeur inférieure à celle requise .priority
afin de pouvoir la "pousser" vers le haut et hors de l'écran.
Nous donnons également à la vue extensible une contrainte de hauteur supérieure ou égale à 350. Cela lui permettra de s'étirer - mais pas de se comprimer - verticalement.
Nous appellerons la vue de UIHostingViewController
notre "contentView"... et nous contraindrons son haut au bas de la vue extensible.
Ensuite, nous donnons à la vue de contenu une autre contrainte Top -- cette fois à la vue de défilement .contentLayoutGuide
, avec une constante de 350 (la hauteur de la vue extensible). Ceci, plus les contraintes Leading/Trailing/Bottom définit la "zone de défilement".
Lorsque nous faisons défiler (tirer) vers le bas, la vue du contenu "déroulera" le bas de la vue extensible.
Lorsque nous faisons défiler (poussons) vers le haut, la vue du contenu "poussera" toute la vue extensible.
Voici à quoi ça ressemble (trop gros pour être ajouté comme gif ici): https://imgur.com/a/wkThhzN
Et voici l'exemple de code pour le faire. Tout se fait via le code, donc pas @IBOutlet
ou d'autres connexions nécessaires. Notez également que j'ai utilisé trois images pour les pages vues - "ex1", "ex2", "ex3":
Afficher le contrôleur
class StretchyHeaderViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.contentInsetAdjustmentBehavior =.never
return v
}()
let stretchyView: UIView = {
let v = UIView()
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor =.systemYellow
return v
}()
let stretchyViewHeight: CGFloat = 350.0
override func viewDidLoad() {
super.viewDidLoad()
// set to a greter-than-zero value if you want spacing between the "pages"
let opts = [UIPageViewController.OptionsKey.interPageSpacing: 0.0]
// instantiate the Page View controller
let pgVC = SamplePageViewController(transitionStyle:.scroll, navigationOrientation:.horizontal, options: opts)
// add it as a child controller
self.addChild(pgVC)
// safe unwrap
guard let pgv = pgVC.view else { return }
pgv.translatesAutoresizingMaskIntoConstraints = false
// add the page controller view to stretchyView
stretchyView.addSubview(pgv)
pgVC.didMove(toParent: self)
NSLayoutConstraint.activate([
// constrain page view controller's view on all 4 sides
pgv.topAnchor.constraint(equalTo: stretchyView.topAnchor),
pgv.bottomAnchor.constraint(equalTo: stretchyView.bottomAnchor),
pgv.centerXAnchor.constraint(equalTo: stretchyView.centerXAnchor),
pgv.widthAnchor.constraint(equalTo: stretchyView.widthAnchor),
])
[scrollView, stretchyView, contentView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// add contentView and stretchyView to the scroll view
[stretchyView, contentView].forEach { v in
scrollView.addSubview(v)
}
// add scroll view to self.view
view.addSubview(scrollView)
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
// keep stretchyView's Top "pinned" to the Top of the scroll view FRAME
// so its Height will "stretch" when scroll view is pulled down
let stretchyTop = stretchyView.topAnchor.constraint(equalTo: frameG.topAnchor, constant: 0.0)
// priority needs to be less-than-required so we can "push it up" out of view
stretchyTop.priority =.defaultHigh
NSLayoutConstraint.activate([
// scroll view Top to view Top
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0),
// scroll view Leading/Trailing/Bottom to safe area
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
// constrain stretchy view Top to scroll view's FRAME
stretchyTop,
// stretchyView to Leading/Trailing of scroll view FRAME
stretchyView.leadingAnchor.constraint(equalTo: frameG.leadingAnchor, constant: 0.0),
stretchyView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: 0.0),
// stretchyView Height - greater-than-or-equal-to
// so it can "stretch" vertically
stretchyView.heightAnchor.constraint(greaterThanOrEqualToConstant: stretchyViewHeight),
// content view Leading/Trailing/Bottom to scroll view's CONTENT GUIDE
contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// content view Width to scroll view's FRAME
contentView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: 0.0),
// content view Top to scroll view's CONTENT GUIDE
// plus Height of stretchyView
contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: stretchyViewHeight),
// content view Top to stretchyView Bottom
contentView.topAnchor.constraint(equalTo: stretchyView.bottomAnchor, constant: 0.0),
])
// add some content to the content view so we have something to scroll
addSomeContent()
}
func addSomeContent() {
// vertical stack view with 20 labels
// so we have something to scroll
let stack = UIStackView()
stack.axis =.vertical
stack.spacing = 32
stack.backgroundColor =.gray
stack.translatesAutoresizingMaskIntoConstraints = false
for i in 1...20 {
let v = UILabel()
v.text = "Label \(i)"
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.heightAnchor.constraint(equalToConstant: 48.0).isActive = true
stack.addArrangedSubview(v)
}
contentView.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0),
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0),
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16.0),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0),
])
}
}
Contrôleur pour chaque page
class OnePageVC: UIViewController {
var image: UIImage = UIImage() {
didSet {
imgView.image = image
}
}
let imgView: UIImageView = {
let v = UIImageView()
v.backgroundColor =.systemBlue
v.contentMode =.scaleAspectFill
v.clipsToBounds = true
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor =.systemBackground
view.addSubview(imgView)
NSLayoutConstraint.activate([
// constrain image view to all 4 sides
imgView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0),
imgView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
imgView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
imgView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
])
}
}
Exemple de contrôleur de vue de page
class SamplePageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
var controllers: [UIViewController] = []
override func viewDidLoad() {
super.viewDidLoad()
let imgNames: [String] = [
"ex1", "ex2", "ex3",
]
for i in 0..<imgNames.count {
let aViewController = OnePageVC()
if let img = UIImage(named: imgNames[i]) {
aViewController.image = img
}
self.controllers.append(aViewController)
}
self.dataSource = self
self.delegate = self
self.setViewControllers([controllers[0]], direction:.forward, animated: false)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController), index > 0 {
return controllers[index - 1]
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController), index < controllers.count - 1 {
return controllers[index + 1]
}
return nil
}
}
Commentaires
Enregistrer un commentaire