我的 iOS 工具箱:常用 Extension/UI 技巧/Tools(持續更新)

Ethan
8 min readOct 7, 2021

--

Photo by Todd Quackenbush on Unsplash

Extensions

UIViewController Extension

設定 status bar / tab bar 圖片 / 加入 child view controller / 移除 child view controller

extension UIViewController {
func setStatusBar() {
let statusBarSize = UIApplication.shared.statusBarFrame.size // 已淘汰但仍OK
let frame = CGRect(origin: .zero, size: statusBarSize)
let statusbarView = UIView(frame: frame)
statusbarView.backgroundColor = .systemBackground
view.addSubview(statusbarView)
}

func setTabBarImage(imageName: String, title: String) {
let configuration = UIImage.SymbolConfiguration(scale: .large)
let image = UIImage(systemName: imageName, withConfiguration: configuration)
tabBarItem = UITabBarItem(title: title, image: image, tag: 0)
}

func add(_ child: UIViewController) {
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
func remove() {
guard parent != nil else { return }
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}

UIView — Extension

設定 AutoLayout Constraints

extension UIView {

func anchor(
top: NSLayoutYAxisAnchor? = nil,
leading: NSLayoutXAxisAnchor? = nil,
bottom: NSLayoutYAxisAnchor? = nil,
trailing: NSLayoutXAxisAnchor? = nil,
centerY: NSLayoutYAxisAnchor? = nil,
centerX: NSLayoutXAxisAnchor? = nil,
width: CGFloat? = nil,
height: CGFloat? = nil,
padding: UIEdgeInsets = .zero
)
{
self.translatesAutoresizingMaskIntoConstraints = false

if let top = top {
self.topAnchor.constraint(equalTo: top, constant: padding.top).isActive = true
}
if let leading = leading {
self.leadingAnchor.constraint(equalTo: leading, constant: padding.left).isActive = true
}
if let bottom = bottom {
self.bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom).isActive = true
}
if let trailing = trailing {
self.trailingAnchor.constraint(equalTo: trailing, constant: -padding.right).isActive = true
}

if let centerY = centerY {
self.centerYAnchor.constraint(equalTo: centerY).isActive = true
}
if let centerX = centerX {
self.centerXAnchor.constraint(equalTo: centerX).isActive = true
}

if let width = width {
self.widthAnchor.constraint(equalToConstant: width).isActive = true
}
if let height = height {
self.heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
func fillSuperview(padding: UIEdgeInsets = .zero) {
translatesAutoresizingMaskIntoConstraints = false
if let superviewTopAnchor = superview?.topAnchor {
topAnchor.constraint(equalTo: superviewTopAnchor, constant: padding.top).isActive = true
}

if let superviewBottomAnchor = superview?.bottomAnchor {
bottomAnchor.constraint(equalTo: superviewBottomAnchor, constant: -padding.bottom).isActive = true
}

if let superviewLeadingAnchor = superview?.leadingAnchor {
leadingAnchor.constraint(equalTo: superviewLeadingAnchor, constant: padding.left).isActive = true
}

if let superviewTrailingAnchor = superview?.trailingAnchor {
trailingAnchor.constraint(equalTo: superviewTrailingAnchor, constant: -padding.right).isActive = true
}
}

func centerInSuperview(size: CGSize = .zero) {
translatesAutoresizingMaskIntoConstraints = false
if let superviewCenterXAnchor = superview?.centerXAnchor {
centerXAnchor.constraint(equalTo: superviewCenterXAnchor).isActive = true
}

if let superviewCenterYAnchor = superview?.centerYAnchor {
centerYAnchor.constraint(equalTo: superviewCenterYAnchor).isActive = true
}

if size.width != 0 {
widthAnchor.constraint(equalToConstant: size.width).isActive = true
}

if size.height != 0 {
heightAnchor.constraint(equalToConstant: size.height).isActive = true
}
}

func centerXInSuperview() {
translatesAutoresizingMaskIntoConstraints = false
if let superViewCenterXAnchor = superview?.centerXAnchor {
centerXAnchor.constraint(equalTo: superViewCenterXAnchor).isActive = true
}
}

func centerYInSuperview() {
translatesAutoresizingMaskIntoConstraints = false
if let centerY = superview?.centerYAnchor {
centerYAnchor.constraint(equalTo: centerY).isActive = true
}
}

func constrainWidth(constant: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
widthAnchor.constraint(equalToConstant: constant).isActive = true
}

func constrainHeight(constant: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
heightAnchor.constraint(equalToConstant: constant).isActive = true
}

}

UIImage — Extension

改變圖片尺寸

extension UIImage {
func resize(to goalSize: CGSize) -> UIImage? {
let widthRatio = goalSize.width / size.width
let heightRatio = goalSize.height / size.height
let ratio = widthRatio < heightRatio ? widthRatio : heightRatio

let resizedSize = CGSize(width: size.width * ratio, height: size.height * ratio)
UIGraphicsBeginImageContextWithOptions(resizedSize, false, 0.0)
draw(in: CGRect(origin: .zero, size: resizedSize))

let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

return resizedImage
}
}

UIColor — Extension

設定顏色

extension UIColor {
static func rgb(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 1) -> UIColor {
return .init(red: red/255, green: green/255, blue: blue/255, alpha: alpha)
}

static let color1 = UIColor.rgb(red: 250, green: 81, blue: 102)
static let color2 = UIColor.rgb(red: 62, green: 176, blue: 254)

}

UIFont — Extension

設定 SymbolicTraits

extension UIFont {
func withTraits(traits: UIFontDescriptor.SymbolicTraits) -> UIFont {
let descriptor = fontDescriptor.withSymbolicTraits(traits)
return UIFont(descriptor: descriptor!, size: 0)
//size 0 means keep the size as it is
}
func bold() -> UIFont {
return withTraits(traits: .traitBold)
}

func italic() -> UIFont {
return withTraits(traits: .traitItalic)
}
}

字型加入(在文章最下面)

Decimal — Extension

十進位小數轉 Double

extension Decimal {
var doubleValue: Double {
return NSDecimalNumber(decimal:self).doubleValue
}
}

Data — Extension

美化 JSON String

extension Data {
func prettyPrintedJSONString() {
guard
let jsonObject = try? JSONSerialization.jsonObject(with: self, options: []),
let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted]),
let prettyJSONString = String(data: jsonData, encoding: .utf8) else {
print("Failed to read JSON Object.")
return
}
print(prettyJSONString)
}
}

UI 技巧

Table View

Multiple Cells in a Table View

Collection View

Flow Layout

Compositional Layout

Page View Controller; Tab Bar Controller

Scroll View

Text Field

leftView / rightView 插入 icon

extension UITextField {
func setIcon(_ image: UIImage) {
let iconView = UIImageView(frame: CGRect(x: 5, y: 5, width: 20, height: 20))
iconView.tintColor = .systemGreen
iconView.image = image

let iconContainerView = UIView(frame: CGRect(x: 10, y: 0, width: 30, height: 30))
iconContainerView.addSubview(iconView)

leftView = iconContainerView
leftViewMode = .always
}
}

#06 能輸入單行文字的 UITextField

Text View

padding 調整

bodyTextView.textContainerInset = UIEdgeInsets(top: 30, left: 15, bottom: 30, right: 15)

#07 能輸入多行文字的 UITextView

Stack View

快速加入

func makeVerticalStackView() -> UIStackView {
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
stack.spacing = 8
return stack
}

padding 調整

horizontalStackView.isLayoutMarginsRelativeArrangement = true
horizontalStackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 15)

Label

快速加入

func makeLabel(withTitle title: String, textColor: UIColor, font: UIFont) -> UILabel {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = title
label.font = font
label.textAlignment = .center
label.textColor = textColor
label.numberOfLines = 0
label.adjustsFontSizeToFitWidth = true
return label
}

客製具有 EdgeInsets 的 label

class CategoryLabel: UILabel {

let topInset = CGFloat(4)
let bottomInset = CGFloat(4)
let leftInset = CGFloat(8)
let rightInset = CGFloat(8)

override func drawText(in rect: CGRect) {
let insets: UIEdgeInsets = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
super.drawText(in: rect.inset(by: insets))
}

override public var intrinsicContentSize: CGSize {
var intrinsicSuperViewContentSize = super.intrinsicContentSize
intrinsicSuperViewContentSize.height += topInset + bottomInset
intrinsicSuperViewContentSize.width += leftInset + rightInset
return intrinsicSuperViewContentSize
}

}

#05 能顯示文字的 UILabel

Button

一般標題按鈕快速加入

func makeXXXButton(withText title: String) -> UIButton {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false

button.setTitle(title, for: .normal)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .title3)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.titleLabel?.minimumScaleFactor = 0.5

button.backgroundColor = .XXXGreen
button.layer.cornerRadius = XXXButtonSpacing.height/2
button.contentEdgeInsets = UIEdgeInsets(top: 10, left: ScanButtonSpacing.height, bottom: 10, right: ScanButtonSpacing.height) //iOS15淘汰 return button
}
struct XXXButtonSpacing {
static let height: CGFloat = 60
static let width: CGFloat = 170
}

Attributed-title 按鈕快速加入

func makeXXXButton(withText title: String) -> UIButton {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false

let attributedText = NSMutableAttributedString(string: title, attributes: [
.font: UIFont.boldSystemFont(ofSize: 16),
.foregroundColor: UIColor.white,
.kern: 2
])
button.setAttributedTitle(attributedText, for: .normal)
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.titleLabel?.minimumScaleFactor = 0.5

button.backgroundColor = .XXXGreen
button.layer.cornerRadius = ScanButtonSpacing.height/2
button.contentEdgeInsets = UIEdgeInsets(top: 10, left: ScanButtonSpacing.height, bottom: 10, right: ScanButtonSpacing.height) //iOS15淘汰

return button
}
struct XXXButtonSpacing {
static let height: CGFloat = 60
static let width: CGFloat = 170
}

「圖片與一般標題並排」按鈕快速加入

func makeXXXButton(systemName: String, text: String) -> UIButton {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false

let configuration = UIImage.SymbolConfiguration(scale: .default)
let image = UIImage(systemName: systemName, withConfiguration: configuration)
button.setImage(image, for: .normal)
button.imageView?.tintColor = .secondaryLabel
button.imageView?.contentMode = .scaleAspectFit


button.setTitle(text, for: .normal)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .caption1)
button.setTitleColor(.secondaryLabel, for: .normal)
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.titleLabel?.minimumScaleFactor = 0.5

button.backgroundColor = .systemGray6
button.layer.cornerRadius = XXXButtonSpacing.height/2
button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) //iOS15淘汰
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 6) //iOS15淘汰

return button
}
struct XXXButtonSpacing {
static let height: CGFloat = 30
static let width: CGFloat = 80
}

純程式按鈕與 View Model

用 IBDesignable 和 IBInspectable 建立可手動調整按鈕 cornerRadius/borderWidth/borderColor/左右上下 titlePadding 的圖形介面

import UIKit@IBDesignable
class FancyButton: UIButton {
// cornerRadius, borderWidth, borderColor
@IBInspectable var cornerRadius: CGFloat = 0.0 {
didSet {
layer.cornerRadius = cornerRadius
layer.masksToBounds = cornerRadius > 0
}
}
@IBInspectable var borderWidth: CGFloat = 0.0 {
didSet {
layer.borderWidth = borderWidth
}
}
@IBInspectable var borderColor: UIColor = .black {
didSet {
layer.borderColor = borderColor.cgColor
}
}

// Paddings
@IBInspectable var titleLeftPadding: CGFloat = 0.0 {
didSet {
titleEdgeInsets.left = titleLeftPadding
}
}
@IBInspectable var titleRightPadding: CGFloat = 0.0 {
didSet {
titleEdgeInsets.right = titleRightPadding
}
}
@IBInspectable var titleTopPadding: CGFloat = 0.0 {
didSet {
titleEdgeInsets.top = titleTopPadding
}
}
@IBInspectable var titleBottomPadding: CGFloat = 0.0 {
didSet {
titleEdgeInsets.bottom = titleBottomPadding
}
}
}

NavigationBar / TabBar

樣式

漸層背景

獨立成一個 GradientView

import UIKitfinal class GradientView: UIView {

override class var layerClass: AnyClass {
return CAGradientLayer.classForCoder()
}

init(colors: [UIColor]) {
super.init(frame: .zero)
let gradientLayer = layer as! CAGradientLayer
gradientLayer.colors = colors.map { $0.cgColor }
gradientLayer.startPoint = CGPoint(x: 0.3, y: 0)
gradientLayer.endPoint = CGPoint(x: 0.7, y: 1)
gradientLayer.locations = [-0.2, 1.2]

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

}

直接加

    let layer       = CAGradientLayer()
let startColor = UIColor.rgb(red: 255, green: 101, blue: 91).cgColor
let endColor = UIColor.rgb(red: 253, green: 41, blue: 123).cgColor
layer.colors = [startColor, endColor]
layer.locations = [0.3, 1.0]
layer.frame = view.bounds
view.layer.addSublayer(layer)

小鍵盤相關

鍵盤出現與消失調整 NSLayoutConstraint

Causes the view (or one of its embedded text fields) to resign the first responder status.

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
view.endEditing(true)
}

登入流程相關與 Firebase 增刪查改

純程式

Storyboard

Tools

Preview 功能

View Controller 預覽

#if canImport(SwiftUI) && DEBUG
import SwiftUI
@available(iOS 13.0, *)
struct UIViewControllerPreview<ViewController: UIViewController>: UIViewControllerRepresentable {
let viewController: ViewController
init(_ builder: @escaping () -> ViewController) {
viewController = builder()
}
func makeUIViewController(context: Context) -> some UIViewController {
viewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: UIViewControllerRepresentableContext<UIViewControllerPreview<ViewController>>) {
return
}
}
#endif
#if canImport(SwiftUI) && DEBUG
import SwiftUI
// 可加入多個裝置
let deviceNames: [String] = [
"iPhone 13 Pro Max"
]
@available(iOS 13.0, *)
struct ViewController_Previews: PreviewProvider {
static var previews: some View {
ForEach(deviceNames, id: \.self) { deviceName in
UIViewControllerPreview {
ViewController()
}
.previewDevice(PreviewDevice(rawValue: deviceName))
.previewDisplayName(deviceName)
}.previewInterfaceOrientation(.portrait)
}
}
#endif

View 預覽

#if canImport(SwiftUI) && DEBUG
import SwiftUI
@available(iOS 13, *)
public struct UIViewPreview<View: UIView>: UIViewRepresentable {
public let view: View
public init(_ builder: @escaping () -> View) {
view = builder()
}
public func makeUIView(context: Context) -> UIView {
return view
}
public func updateUIView(_ view: UIView, context: Context) {
view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
view.setContentHuggingPriority(.defaultHigh, for: .vertical)
}
}
#endif
#if canImport(SwiftUI) && DEBUG
import SwiftUI
@available(iOS 13.0, *)
struct SimpleView_Preview: PreviewProvider {
static var previews: some View {
UIViewPreview {
let button = XXXButton()
return button

}
.previewLayout(.sizeThatFits)
.padding(10.0)
}
}
#endif

Xcode 13 No Storyboards

var window: UIWindow?func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
window?.backgroundColor = .systemBackground
window?.rootViewController = ViewController()

return true
}

--

--

Ethan

Life is what happens to you while you’re busy making other plans.