Swift新手求教:自定义UIPageControl的代码原理及简化实现
Hey there! Let's break down this custom CZPageControl code step by step, and I'll also share a simpler, more beginner-friendly approach since you're new to Swift.
CZPageControl Code This code is built to handle iOS 14+ and older versions because Apple completely changed the internal structure of UIPageControl starting in iOS 14. Let's unpack each part:
1. Core Customization Properties
These are the settings you'll use to style the page control:
currentImage/inactiveImage: The images for selected and unselected dots.currentTintColor/inactiveTintColor: The color applied to those images (we use.alwaysTemplaterendering mode, so the tint overrides the image's original color).
2. Initialization
init(frame:): Disables user interaction (page controls are usually just for display, not tapping to switch pages) and turns off continuous interaction for iOS 14+.init(coder:)is marked fatal because this class is meant to be created programmatically, not from Storyboards/XIBs.
3. Triggering Updates
The currentPage property has a didSet observer that calls updateDots() every time the page changes. This is what makes the dots refresh when you switch pages.
4. Updating Dots (The Core Logic)
The updateDots() method splits into two paths based on iOS version:
For iOS 14+
Apple rewrote UIPageControl's internal layout, so we can't directly access dot views anymore:
findIndicatorContentView()searches for a private system view (_UIPageControlIndicatorContentView) that holds all the dot indicators.- We loop through this container's subviews (each is a
UIImageView), clear existing images, then setcurrentImagefor the selected page andinactiveImagefor others. Tint colors are applied here too.
For iOS 13 and Below
Older UIPageControl uses simple subviews for each dot:
- We loop through the page control's subviews, and for each dot, use
imageViewForSubview()to reuse an existingUIImageViewor create a new one. - We set the appropriate image and tint color, then adjust the
UIImageView's frame to center it inside the original dot view.
5. Helper Methods
imageViewForSubview(_:currentPage:): For iOS <14, this ensures each dot has anUIImageViewto display our custom image (replacing the default dot).findIndicatorContentView(): For iOS 14+, this digs into private system views to find where dots are stored (note: using private APIs is risky if Apple changes internals, but it's common for customizing system controls).
If you're new to Swift, relying on private system views can be confusing and fragile. Instead, build a custom page control from scratch using UICollectionView—it's straightforward and gives you full control.
Here's a clean implementation:
import UIKit class SimplePageControl: UIView { // Configure these to customize the look var numberOfPages: Int = 0 { didSet { collectionView.reloadData() } } var currentPage: Int = 0 { didSet { collectionView.reloadData() // Optional: Scroll to keep the current dot centered let indexPath = IndexPath(item: currentPage, section: 0) collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) } } var currentDotSize: CGSize = CGSize(width: 16, height: 8) var inactiveDotSize: CGSize = CGSize(width: 8, height: 8) var currentDotColor: UIColor = .systemBlue var inactiveDotColor: UIColor = .systemGray3 var dotSpacing: CGFloat = 8 private let collectionView: UICollectionView override init(frame: CGRect) { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.minimumInteritemSpacing = dotSpacing collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) super.init(frame: frame) setup() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setup() { collectionView.translatesAutoresizingMaskIntoConstraints = false addSubview(collectionView) // Pin collectionView to our view's edges NSLayoutConstraint.activate([ collectionView.topAnchor.constraint(equalTo: topAnchor), collectionView.bottomAnchor.constraint(equalTo: bottomAnchor), collectionView.leadingAnchor.constraint(equalTo: leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: trailingAnchor) ]) collectionView.dataSource = self collectionView.delegate = self collectionView.showsHorizontalScrollIndicator = false collectionView.isUserInteractionEnabled = false // Disable if you don't want tapping to switch pages collectionView.register(DotCell.self, forCellWithReuseIdentifier: DotCell.reuseID) } } // MARK: - UICollectionView DataSource & Delegate extension SimplePageControl: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { numberOfPages } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DotCell.reuseID, for: indexPath) as! DotCell let isCurrent = indexPath.item == currentPage cell.configure(isCurrent: isCurrent, currentSize: currentDotSize, inactiveSize: inactiveDotSize, currentColor: currentDotColor, inactiveColor: inactiveDotColor) return cell } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { indexPath.item == currentPage ? currentDotSize : inactiveDotSize } } // MARK: - Dot Cell class DotCell: UICollectionViewCell { static let reuseID = "DotCell" private let dotView = UIView() override init(frame: CGRect) { super.init(frame: frame) setupDotView() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupDotView() { dotView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(dotView) NSLayoutConstraint.activate([ dotView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), dotView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) ]) } func configure(isCurrent: Bool, currentSize: CGSize, inactiveSize: CGSize, currentColor: UIColor, inactiveColor: UIColor) { dotView.backgroundColor = isCurrent ? currentColor : inactiveColor dotView.layer.cornerRadius = isCurrent ? currentSize.height / 2 : inactiveSize.height / 2 // Update dot size constraints NSLayoutConstraint.activate([ dotView.widthAnchor.constraint(equalToConstant: isCurrent ? currentSize.width : inactiveSize.width), dotView.heightAnchor.constraint(equalToConstant: isCurrent ? currentSize.height : inactiveSize.height) ]) } }
Why This Is Better for Beginners
- No private APIs: You don't have to rely on system internals that might change.
- Full control: Easily add animations, change dot shapes (e.g., squares), or adjust spacing without worrying about iOS versions.
- Clear logic: The
UICollectionViewpattern is a core UIKit concept, so learning this will help with other iOS development tasks.
To use it, create an instance, set numberOfPages, and update currentPage when your main page view changes—super straightforward!
内容的提问来源于stack exchange,提问作者yude

