App 中想加入一个订阅页面,期望 iOS 上按 bottom sheet 展示,iPad 上居中显示,同时支持横屏和竖屏,效果如下:

iOS 15 提供了 UISheetPresentationController
可以令 Bottom Sheet 更方便,具体可参考How to present a Bottom Sheet in iOS 15还有Presenting sheets with UIKit using a UISheetPresentationController。
UISheetPresentationController
不满足需求因为它只有 medium
和 large
两种高度,并且宽度是满屏,不符合需求。
通过 OpenAI
问答,找到了可以满足需求的方法来在一个页面中弹出订阅页面。
然后不出意外的,新问题又出现了:想要在另外一个页面中也弹出同样的订阅页面,当时这两个页面的父类是不同 UIViewController
,也就是说没办法给他们定义一个共同的定制化的 UIViewController
子类,而 Swift
又不支持多继承,尝试了带默认实现的 Protocol
,无法实现。最终还是通过询问 AI
找到了实现方法:

最终实现:¶
SubscriptionSheetPresenter.swift
共享的 presenter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class SubscriptionSheetPresenter: NSObject {
static let instance = SubscriptionSheetPresenter()
func presentBottomSheetForSubscription(from vc: UIViewController) {
// storyboard 中定义了定制页面的布局
let dashboard = UIStoryboard.init(name: "Main", bundle: nil)
guard let subscriptionVC = dashboard.instantiateViewController(withIdentifier: "subscriptionViewController") as? SubscriptionViewController else {
return
}
subscriptionVC.modalPresentationStyle = .custom
subscriptionVC.transitioningDelegate = self
vc.present(subscriptionVC, animated: true, completion: nil)
}
}
// MARK: - Bottom Sheet for Subscription
extension SubscriptionSheetPresenter: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return SubscriptionSheetPresentationController(presentedViewController: presented, presenting: presenting)
}
}
|
SubscriptionViewController.swift
Storyboard 中订阅页面的 UIViewController class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class SubscriptionViewController: UIViewController {
@IBOutlet weak var monthlyBuy: UIButton!
@IBOutlet weak var yearlyBuy: UIButton!
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
updateUI()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
updateUI()
}
private func updateUI() {
// update layour for diff device (iPhone/iPad) and view (Landscape/Protraint)
// ...
}
|
SubscriptionSheetPresentationController.swift
定制订阅页面的宽度和高度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
class SubscriptionSheetPresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
// default is iPhone+portrait for non-SE
var width: CGFloat = screenWidth
var height: CGFloat = screenHeight * 2/3
var x: CGFloat = 0
var yOffset = 1.0
if UIDevice.current.userInterfaceIdiom == .phone {
if UIDevice.current.orientation.isLandscape {
width = screenWidth / 2
height = screenHeight * 0.95
} else {
if screenHeight < 700 {
height = screenHeight * 2.5/3
}
}
} else { // iPad or Mac
if UIDevice.current.orientation.isLandscape {
width = screenWidth / 2.5
height = screenHeight * 0.65
} else {
width = screenWidth / 1.8
height = screenHeight * 0.5
}
yOffset = 0.5
}
x = (screenWidth - width)/2
return CGRect(x: x,
y: (containerView.bounds.height - height) * yOffset,
width: width,
height: height)
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
if let presentedView = presentedView {
presentedView.frame = frameOfPresentedViewInContainerView
}
}
}
|
在需要弹出订阅页面的 UIViewController
中直接调用即可:
<Any>ViewController.swift
1
2
3
4
5
|
class <Any>ViewController: <XXX>ViewController {
func showSubscription() {
SubscriptionSheetPresenter.instance.presentBottomSheetForSubscription(from: self)
}
}
|
<Another>ViewController.swift
1
2
3
4
5
|
class <Anyother>ViewController: <YYY>ViewController {
func goPremium() {
SubscriptionSheetPresenter.instance.presentBottomSheetForSubscription(from: self)
}
}
|
Usefull links¶