【 iOS Swift 】#17 App的教學導覽頁 Onboarding View (純Code)
在安裝新App 時,是不是時常會看見跳出的功能導覽圖,教你如何使用 App,或是告訴你這次新功能有哪些。
成品如下:
彼得這兒有清晰的 StoryBoard 教學,可以先把 StoryBoard 熟練,
再跟著我一起往下做純 Code~
如果想要抓現成的圖來練習,可以到Figma搜尋「Onboarding View 相關」
把人家的設計稿Duplicate一份後,可以先把一些元件拿掉,只取圖檔的部分,其他元件自己用Swift加。
我選擇使用的是:
假如我希望使用者在進入首頁前,可以先逼他看完教學導覽圖,我就可以寫在我的 HomePageViewController的viewDidAppear:
import UIKit
class HomePageViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
// 在首頁的VC顯示完馬上顯示教學導覽
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
// 希望他只出現一次,所以用UserDefault儲存使用者看過了沒
let userDefault = UserDefaults.standard
if (!userDefault.bool(forKey: "hasSeenTutorial")) {
// 把教學導覽的VC生出來,並全螢幕Present出來
let tutorialVC = TutorialViewController()
tutorialVC.setUserDefaultKey("hasSeenTutorial")
tutorialVC.modalPresentationStyle = .fullScreen
self.present(tutorialVC, animated: true)
}
}
}
其中,TutorialViewController的主要概念是透過ScrollView + StackView + UIView的方式來達成,如上圖所示。
接著,跟著我一步一步打造TutorialViewController:
- 建立元件
- 加入 scroll view,佔滿整個螢幕
- 加入 stack view,並讓其與 scroll view 的上下左右間距為 0
- 加入Page Control、Button等元件,設定每一頁按鈕按下的 Action
- 製作每一個分頁的UI,讓每一個分頁的UI的寬高等於螢幕的寬高
- 設定當手勢滑動時,更新目前分頁 Index
- 強制讓使用者當下的上方時間、電池訊號列變成白色
一、建立元件
import UIKit
class TutorialViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.isPagingEnabled = true
return scrollView
}()
let stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 0
stackView.distribution = .fillEqually
return stackView
}()
let controlStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = 0
stackView.distribution = .equalSpacing
return stackView
}()
let pageControl: UIPageControl = {
let pageControl = UIPageControl()
pageControl.isUserInteractionEnabled = false
pageControl.pageIndicatorTintColor = .darkGray
pageControl.currentPageIndicatorTintColor = .white
pageControl.allowsContinuousInteraction = false
return pageControl
}()
// 右側往下一頁的按鈕
let circleButton: UIButton = {
let button = UIButton()
if let image = UIImage(systemName: "arrow.right") {
button.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal)
button.tintColor = .black
}
button.backgroundColor = .white
return button
}()
// 首、尾頁的按鈕
let pillButton: UIButton = {
let button = UIButton()
button.setTitle("Let's Discover", for: .normal)
button.backgroundColor = .white
button.setTitleColor(.black, for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
return button
}()
...
二、加入 scroll view,佔滿整個螢幕
func setupUI() {
view.addSubview(scrollView)
scrollView.delegate = self
scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
...
三、加入 stack view,並讓其與 scroll view 的上下左右間距為 0
(Content Layout Guide:ScrollView決定捲動的範圍)
scrollView.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor)
])
四、加入Page Control、Button等元件,設定每一頁按鈕按下的 Action
// ControlStackView = PageControl + CircleButton
view.addSubview(controlStackView)
controlStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
controlStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
controlStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 36),
controlStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -36),
controlStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -72),
])
// PageControl
// 記得加東西到stackView上都是用addArrangedSubview不是addSubview了
controlStackView.addArrangedSubview(pageControl)
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.numberOfPages = tutorialImages.count
pageControl.currentPage = currentIndex
// CircleButton
controlStackView.addArrangedSubview(circleButton)
NSLayoutConstraint.activate([
circleButton.widthAnchor.constraint(equalToConstant: 54),
circleButton.heightAnchor.constraint(equalToConstant: 54)
])
circleButton.layer.cornerRadius = 27
circleButton.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
controlStackView.isHidden = true
// PillButton
view.addSubview(pillButton)
pillButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
pillButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
pillButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 36),
pillButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -36),
pillButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -72),
pillButton.heightAnchor.constraint(equalToConstant: 54),
])
pillButton.layer.cornerRadius = 27
pillButton.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
按下按鈕時的Action:
@objc func buttonPressed() {
// 如果不最後一頁,就Index + 1
if currentIndex < pageViews.count-1 {
currentIndex += 1
scrollView.setContentOffset(pageViews[currentIndex].frame.origin, animated: true)
} else if currentIndex == pageViews.count-1 {
// 最後一頁,要關閉這個VC
self.dismiss(animated: true, completion: nil)
UserDefaults.standard.set(true, forKey: userDefaultKey)
}
pageControl.currentPage = currentIndex
}
透過Property Observer,來讓特地某幾頁的按鈕隱藏與顯示:
var currentIndex: Int = 0 {
didSet {
// 首、尾頁按鈕文字不同
if currentIndex == 0 {
pillButton.isHidden = false
controlStackView.isHidden = true
pillButton.setTitle("Let's Discover", for: .normal)
}
// 除了首、尾頁需要PillButton,其餘則是PageControl配CircleButton
else if currentIndex > 0 && currentIndex < tutorialImages.count-1 {
pillButton.isHidden = true
controlStackView.isHidden = false
}
else {
pillButton.isHidden = false
controlStackView.isHidden = true
pillButton.setTitle("Get Started", for: .normal)
}
print("目前第\(currentIndex)頁")
}
}
五、製作每一個分頁的UI,讓每一個分頁的UI的寬高等於螢幕的寬高
把圖檔放到專案左側欄的Image.asset,接著定義每張圖檔的Array
let tutorialImages = ["Onboarding_1", "Onboarding_2", "Onboarding_3"]
用for重複建構每一頁的擺設,每一頁都是一個UIView,UIView上面放著滿版Image。組裝好後把每一頁一一加到stackView上,並讓每一頁的寬、高將貼齊ScrollView的frameLayoutGuide。
(Frame Layout Guide:ScrollView顯示的範圍)
func addPageView() {
for image in tutorialImages {
let pageView = UIView()
let imageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: image))
imageView.contentMode = .scaleAspectFill
return imageView
}()
pageView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: pageView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: pageView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: pageView.trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: pageView.bottomAnchor)
])
stackView.addArrangedSubview(pageView)
pageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
pageView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
pageView.heightAnchor.constraint(equalTo: scrollView.frameLayoutGuide.heightAnchor)
])
pageViews.append(pageView)
}
}
六、設定當手勢滑動時,更新目前分頁 Index
同時也務必讓此VC的class繼承UIScrollViewDelegate,以及scrollView.delegate = self。
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageWidth = scrollView.frame.size.width
let currentPage = Int(scrollView.contentOffset.x / pageWidth)
currentIndex = currentPage
print(currentIndex)
}
七、強制讓使用者當下的上方時間、電池訊號列變成白色
// 由於背景圖是深色,把最上方的Status Bar強制調為白色
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
另外,下方這個function為當別人要用此VC時,可以更改儲存的UserDefault Key。用意是,當最後一頁按鈕點下後,才可以去把指定的userDefault設定為false,讓使用者在下次打開APP時不會再被騷擾。
@objc func setUserDefaultKey(_ key: String) {
userDefaultKey = key
}
此外,你可能注意到,為什麼PageControl的圓點為什麼就是一直無法乖乖貼齊邊界呢?卻有怎麼多padding跑出來
你可以這樣用,但此方法僅在iOS13以上可用:
let pageControl: UIPageControl = {
let pageControl = UIPageControl()
pageControl.isUserInteractionEnabled = false
pageControl.pageIndicatorTintColor = .darkGray
pageControl.currentPageIndicatorTintColor = .white
// 讓pageControl的小圓點貼齊邊界,不留空隙
pageControl.backgroundStyle = .minimal
pageControl.allowsContinuousInteraction = false
return pageControl
}()
大功告成!
完整 Swift Code