Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
259
ios-application-dev/references/accessibility.md
Normal file
259
ios-application-dev/references/accessibility.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Accessibility
|
||||
|
||||
iOS accessibility guide covering Dynamic Type, semantic colors, VoiceOver, and motion adaptation.
|
||||
|
||||
## Dynamic Type
|
||||
|
||||
### Using System Fonts
|
||||
|
||||
```swift
|
||||
private func setupLabels() {
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.font = .preferredFont(forTextStyle: .headline)
|
||||
titleLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
let bodyLabel = UILabel()
|
||||
bodyLabel.font = .preferredFont(forTextStyle: .body)
|
||||
bodyLabel.adjustsFontForContentSizeCategory = true
|
||||
bodyLabel.numberOfLines = 0
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Font Scaling
|
||||
|
||||
```swift
|
||||
extension UIFont {
|
||||
static func scaled(_ name: String, size: CGFloat, for style: TextStyle) -> UIFont {
|
||||
guard let font = UIFont(name: name, size: size) else {
|
||||
return .preferredFont(forTextStyle: style)
|
||||
}
|
||||
return UIFontMetrics(forTextStyle: style).scaledFont(for: font)
|
||||
}
|
||||
}
|
||||
|
||||
let customFont = UIFont.scaled("Avenir-Medium", size: 16, for: .body)
|
||||
```
|
||||
|
||||
### Text Style Reference
|
||||
|
||||
| Style | Default Size | Usage |
|
||||
|-------|--------------|-------|
|
||||
| `.largeTitle` | 34pt | Screen titles |
|
||||
| `.title1` | 28pt | Primary headings |
|
||||
| `.title2` | 22pt | Secondary headings |
|
||||
| `.title3` | 20pt | Tertiary headings |
|
||||
| `.headline` | 17pt (semibold) | Important information |
|
||||
| `.body` | 17pt | Body text |
|
||||
| `.callout` | 16pt | Explanatory text |
|
||||
| `.subheadline` | 15pt | Subtitles |
|
||||
| `.footnote` | 13pt | Footnotes |
|
||||
| `.caption1` | 12pt | Labels |
|
||||
| `.caption2` | 11pt | Small labels |
|
||||
|
||||
### Adapting Layout for Large Text
|
||||
|
||||
```swift
|
||||
override func traitCollectionDidChange(_ previous: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previous)
|
||||
|
||||
let isLargeText = traitCollection.preferredContentSizeCategory.isAccessibilityCategory
|
||||
contentStack.axis = isLargeText ? .vertical : .horizontal
|
||||
|
||||
if isLargeText {
|
||||
iconImageView.snp.remakeConstraints { make in
|
||||
make.size.equalTo(64)
|
||||
}
|
||||
} else {
|
||||
iconImageView.snp.remakeConstraints { make in
|
||||
make.size.equalTo(44)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Semantic Colors
|
||||
|
||||
Use system semantic colors for automatic Dark Mode adaptation:
|
||||
|
||||
```swift
|
||||
view.backgroundColor = .systemBackground
|
||||
containerView.backgroundColor = .secondarySystemBackground
|
||||
cardView.backgroundColor = .tertiarySystemBackground
|
||||
|
||||
titleLabel.textColor = .label
|
||||
subtitleLabel.textColor = .secondaryLabel
|
||||
hintLabel.textColor = .tertiaryLabel
|
||||
placeholderLabel.textColor = .placeholderText
|
||||
|
||||
separatorView.backgroundColor = .separator
|
||||
borderView.layer.borderColor = UIColor.separator.cgColor
|
||||
```
|
||||
|
||||
### System Color Reference
|
||||
|
||||
| Color | Light Mode | Dark Mode | Usage |
|
||||
|-------|------------|-----------|-------|
|
||||
| `.systemBackground` | White | Black | Main background |
|
||||
| `.secondarySystemBackground` | Light gray | Dark gray | Card/grouped background |
|
||||
| `.tertiarySystemBackground` | Lighter gray | Medium gray | Nested content background |
|
||||
| `.label` | Black | White | Primary text |
|
||||
| `.secondaryLabel` | Gray | Light gray | Secondary text |
|
||||
| `.tertiaryLabel` | Light gray | Dark gray | Auxiliary text |
|
||||
|
||||
### Custom Color Adaptation
|
||||
|
||||
```swift
|
||||
extension UIColor {
|
||||
static let customAccent = UIColor { traitCollection in
|
||||
switch traitCollection.userInterfaceStyle {
|
||||
case .dark:
|
||||
return UIColor(red: 0.4, green: 0.8, blue: 1.0, alpha: 1.0)
|
||||
default:
|
||||
return UIColor(red: 0.0, green: 0.5, blue: 0.8, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## VoiceOver
|
||||
|
||||
### Basic Labels
|
||||
|
||||
```swift
|
||||
let cartButton = UIButton(type: .system)
|
||||
cartButton.setImage(UIImage(systemName: "cart.badge.plus"), for: .normal)
|
||||
cartButton.accessibilityLabel = "Add to cart"
|
||||
|
||||
let ratingView = UIView()
|
||||
ratingView.accessibilityLabel = "Rating: 4 out of 5 stars"
|
||||
|
||||
let closeButton = UIButton()
|
||||
closeButton.accessibilityLabel = "Close"
|
||||
closeButton.accessibilityHint = "Dismisses this dialog"
|
||||
```
|
||||
|
||||
### Custom Accessibility
|
||||
|
||||
```swift
|
||||
class ProductCell: UICollectionViewCell {
|
||||
override var accessibilityLabel: String? {
|
||||
get {
|
||||
return "\(product.name), \(product.price), \(product.isAvailable ? "In stock" : "Out of stock")"
|
||||
}
|
||||
set {}
|
||||
}
|
||||
|
||||
override var accessibilityTraits: UIAccessibilityTraits {
|
||||
get {
|
||||
var traits: UIAccessibilityTraits = .button
|
||||
if product.isSelected {
|
||||
traits.insert(.selected)
|
||||
}
|
||||
return traits
|
||||
}
|
||||
set {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Accessibility Container
|
||||
|
||||
```swift
|
||||
class CustomContainerView: UIView {
|
||||
override var isAccessibilityElement: Bool {
|
||||
get { false }
|
||||
set {}
|
||||
}
|
||||
|
||||
override var accessibilityElements: [Any]? {
|
||||
get {
|
||||
return [titleLabel, actionButton, detailLabel]
|
||||
}
|
||||
set {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### VoiceOver Notifications
|
||||
|
||||
```swift
|
||||
func didLoadContent() {
|
||||
UIAccessibility.post(notification: .screenChanged, argument: headerLabel)
|
||||
}
|
||||
|
||||
func didUpdateStatus() {
|
||||
UIAccessibility.post(notification: .announcement, argument: "Download complete")
|
||||
}
|
||||
```
|
||||
|
||||
## Reduce Motion
|
||||
|
||||
```swift
|
||||
func animateTransition() {
|
||||
let duration: TimeInterval = UIAccessibility.isReduceMotionEnabled ? 0 : 0.3
|
||||
UIView.animate(withDuration: duration) {
|
||||
self.cardView.alpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
func showPopup() {
|
||||
if UIAccessibility.isReduceMotionEnabled {
|
||||
popupView.alpha = 1
|
||||
} else {
|
||||
popupView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
|
||||
popupView.alpha = 0
|
||||
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0) {
|
||||
self.popupView.transform = .identity
|
||||
self.popupView.alpha = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Observing Setting Changes
|
||||
|
||||
```swift
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(reduceMotionChanged),
|
||||
name: UIAccessibility.reduceMotionStatusDidChangeNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
@objc func reduceMotionChanged() {
|
||||
updateAnimationSettings()
|
||||
}
|
||||
```
|
||||
|
||||
## Accessibility Checklist
|
||||
|
||||
### Basic Requirements
|
||||
- [ ] All icon buttons have `accessibilityLabel`
|
||||
- [ ] Custom controls have correct `accessibilityTraits`
|
||||
- [ ] Images have `accessibilityLabel` or marked as decorative
|
||||
- [ ] Forms have clear error messages
|
||||
|
||||
### Dynamic Type
|
||||
- [ ] Using `preferredFont(forTextStyle:)`
|
||||
- [ ] Set `adjustsFontForContentSizeCategory = true`
|
||||
- [ ] Layout adapts at accessibility sizes
|
||||
- [ ] Text is not truncated
|
||||
|
||||
### Color Contrast
|
||||
- [ ] Body text contrast >= 4.5:1
|
||||
- [ ] Large text contrast >= 3:1
|
||||
- [ ] Information not conveyed by color alone
|
||||
|
||||
### Motion
|
||||
- [ ] Respect Reduce Motion setting
|
||||
- [ ] No flashing or rapid animation
|
||||
- [ ] Auto-playing animations can be paused
|
||||
|
||||
### Interaction
|
||||
- [ ] Touch targets >= 44x44pt
|
||||
- [ ] Gestures have alternative actions
|
||||
- [ ] Timeouts can be extended
|
||||
|
||||
---
|
||||
|
||||
*UIKit, VoiceOver, Dynamic Type, and Apple are trademarks of Apple Inc.*
|
||||
350
ios-application-dev/references/graphics-animation.md
Normal file
350
ios-application-dev/references/graphics-animation.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# Graphics & Animation
|
||||
|
||||
iOS graphics and animation guide covering CAShapeLayer, CAGradientLayer, UIBezierPath, and Core Animation.
|
||||
|
||||
## CAShapeLayer
|
||||
|
||||
For custom shapes, paths, and animations:
|
||||
|
||||
```swift
|
||||
class CircularProgressView: UIView {
|
||||
private let trackLayer = CAShapeLayer()
|
||||
private let progressLayer = CAShapeLayer()
|
||||
|
||||
var progress: CGFloat = 0 {
|
||||
didSet { updateProgress() }
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupLayers()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupLayers()
|
||||
}
|
||||
|
||||
private func setupLayers() {
|
||||
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||
let radius = min(bounds.width, bounds.height) / 2 - 10
|
||||
let startAngle = -CGFloat.pi / 2
|
||||
let endAngle = startAngle + 2 * CGFloat.pi
|
||||
|
||||
let circularPath = UIBezierPath(
|
||||
arcCenter: center,
|
||||
radius: radius,
|
||||
startAngle: startAngle,
|
||||
endAngle: endAngle,
|
||||
clockwise: true
|
||||
)
|
||||
|
||||
trackLayer.path = circularPath.cgPath
|
||||
trackLayer.strokeColor = UIColor.systemGray5.cgColor
|
||||
trackLayer.fillColor = UIColor.clear.cgColor
|
||||
trackLayer.lineWidth = 10
|
||||
trackLayer.lineCap = .round
|
||||
layer.addSublayer(trackLayer)
|
||||
|
||||
progressLayer.path = circularPath.cgPath
|
||||
progressLayer.strokeColor = UIColor.systemBlue.cgColor
|
||||
progressLayer.fillColor = UIColor.clear.cgColor
|
||||
progressLayer.lineWidth = 10
|
||||
progressLayer.lineCap = .round
|
||||
progressLayer.strokeEnd = 0
|
||||
layer.addSublayer(progressLayer)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
setupLayers()
|
||||
}
|
||||
|
||||
private func updateProgress() {
|
||||
progressLayer.strokeEnd = progress
|
||||
}
|
||||
|
||||
func animateProgress(to value: CGFloat, duration: TimeInterval = 0.5) {
|
||||
let animation = CABasicAnimation(keyPath: "strokeEnd")
|
||||
animation.fromValue = progressLayer.strokeEnd
|
||||
animation.toValue = value
|
||||
animation.duration = duration
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
progressLayer.strokeEnd = value
|
||||
progressLayer.add(animation, forKey: "progressAnimation")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## UIBezierPath
|
||||
|
||||
### Common Shapes
|
||||
|
||||
```swift
|
||||
let roundedRect = UIBezierPath(
|
||||
roundedRect: bounds,
|
||||
cornerRadius: 12
|
||||
)
|
||||
|
||||
let customCorners = UIBezierPath(
|
||||
roundedRect: bounds,
|
||||
byRoundingCorners: [.topLeft, .topRight],
|
||||
cornerRadii: CGSize(width: 16, height: 16)
|
||||
)
|
||||
|
||||
let triangle = UIBezierPath()
|
||||
triangle.move(to: CGPoint(x: bounds.midX, y: 0))
|
||||
triangle.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
|
||||
triangle.addLine(to: CGPoint(x: 0, y: bounds.maxY))
|
||||
triangle.close()
|
||||
|
||||
let circle = UIBezierPath(
|
||||
arcCenter: CGPoint(x: bounds.midX, y: bounds.midY),
|
||||
radius: bounds.width / 2,
|
||||
startAngle: 0,
|
||||
endAngle: .pi * 2,
|
||||
clockwise: true
|
||||
)
|
||||
```
|
||||
|
||||
### Custom Paths
|
||||
|
||||
```swift
|
||||
let customPath = UIBezierPath()
|
||||
customPath.move(to: CGPoint(x: 0, y: bounds.height))
|
||||
customPath.addCurve(
|
||||
to: CGPoint(x: bounds.width, y: 0),
|
||||
controlPoint1: CGPoint(x: bounds.width * 0.3, y: bounds.height),
|
||||
controlPoint2: CGPoint(x: bounds.width * 0.7, y: 0)
|
||||
)
|
||||
```
|
||||
|
||||
## CAGradientLayer
|
||||
|
||||
### Linear Gradient Button
|
||||
|
||||
```swift
|
||||
class GradientButton: UIButton {
|
||||
private let gradientLayer = CAGradientLayer()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupGradient()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupGradient()
|
||||
}
|
||||
|
||||
private func setupGradient() {
|
||||
gradientLayer.colors = [
|
||||
UIColor.systemBlue.cgColor,
|
||||
UIColor.systemPurple.cgColor
|
||||
]
|
||||
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
|
||||
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
||||
gradientLayer.cornerRadius = 12
|
||||
layer.insertSublayer(gradientLayer, at: 0)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
gradientLayer.frame = bounds
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Gradient Background View
|
||||
|
||||
```swift
|
||||
class GradientBackgroundView: UIView {
|
||||
private let gradientLayer = CAGradientLayer()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupGradient()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupGradient()
|
||||
}
|
||||
|
||||
private func setupGradient() {
|
||||
gradientLayer.colors = [
|
||||
UIColor.systemBackground.cgColor,
|
||||
UIColor.secondarySystemBackground.cgColor
|
||||
]
|
||||
gradientLayer.locations = [0.0, 1.0]
|
||||
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
|
||||
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
|
||||
layer.insertSublayer(gradientLayer, at: 0)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
gradientLayer.frame = bounds
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
gradientLayer.colors = [
|
||||
UIColor.systemBackground.cgColor,
|
||||
UIColor.secondarySystemBackground.cgColor
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Gradient Types
|
||||
|
||||
| Type | Configuration |
|
||||
|------|---------------|
|
||||
| Linear (horizontal) | `startPoint: (0, 0.5)`, `endPoint: (1, 0.5)` |
|
||||
| Linear (vertical) | `startPoint: (0.5, 0)`, `endPoint: (0.5, 1)` |
|
||||
| Diagonal | `startPoint: (0, 0)`, `endPoint: (1, 1)` |
|
||||
| Radial | Use `CAGradientLayer.type = .radial` |
|
||||
|
||||
## Core Animation
|
||||
|
||||
### Basic Animation
|
||||
|
||||
```swift
|
||||
func animateScale() {
|
||||
let animation = CABasicAnimation(keyPath: "transform.scale")
|
||||
animation.fromValue = 1.0
|
||||
animation.toValue = 1.2
|
||||
animation.duration = 0.3
|
||||
animation.autoreverses = true
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
layer.add(animation, forKey: "scaleAnimation")
|
||||
}
|
||||
|
||||
func animatePosition() {
|
||||
let animation = CABasicAnimation(keyPath: "position")
|
||||
animation.fromValue = layer.position
|
||||
animation.toValue = CGPoint(x: 200, y: 200)
|
||||
animation.duration = 0.5
|
||||
layer.add(animation, forKey: "positionAnimation")
|
||||
}
|
||||
```
|
||||
|
||||
### Keyframe Animation
|
||||
|
||||
```swift
|
||||
func animateAlongPath() {
|
||||
let path = UIBezierPath()
|
||||
path.move(to: CGPoint(x: 50, y: 50))
|
||||
path.addCurve(
|
||||
to: CGPoint(x: 250, y: 250),
|
||||
controlPoint1: CGPoint(x: 150, y: 50),
|
||||
controlPoint2: CGPoint(x: 50, y: 250)
|
||||
)
|
||||
|
||||
let animation = CAKeyframeAnimation(keyPath: "position")
|
||||
animation.path = path.cgPath
|
||||
animation.duration = 2.0
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
layer.add(animation, forKey: "pathAnimation")
|
||||
}
|
||||
```
|
||||
|
||||
### Animation Group
|
||||
|
||||
```swift
|
||||
func animateMultiple() {
|
||||
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
|
||||
scaleAnimation.fromValue = 1.0
|
||||
scaleAnimation.toValue = 1.5
|
||||
|
||||
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
|
||||
opacityAnimation.fromValue = 1.0
|
||||
opacityAnimation.toValue = 0.0
|
||||
|
||||
let group = CAAnimationGroup()
|
||||
group.animations = [scaleAnimation, opacityAnimation]
|
||||
group.duration = 0.5
|
||||
group.fillMode = .forwards
|
||||
group.isRemovedOnCompletion = false
|
||||
|
||||
layer.add(group, forKey: "multipleAnimations")
|
||||
}
|
||||
```
|
||||
|
||||
### Spring Animation
|
||||
|
||||
```swift
|
||||
func springAnimation() {
|
||||
let spring = CASpringAnimation(keyPath: "transform.scale")
|
||||
spring.fromValue = 0.8
|
||||
spring.toValue = 1.0
|
||||
spring.damping = 10
|
||||
spring.stiffness = 100
|
||||
spring.mass = 1
|
||||
spring.initialVelocity = 5
|
||||
spring.duration = spring.settlingDuration
|
||||
layer.add(spring, forKey: "springAnimation")
|
||||
}
|
||||
```
|
||||
|
||||
## UIView Animation
|
||||
|
||||
### Basic UIView Animation
|
||||
|
||||
```swift
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.view.alpha = 1.0
|
||||
self.view.transform = .identity
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut]) {
|
||||
self.cardView.frame.origin.y = 100
|
||||
} completion: { _ in
|
||||
self.didFinishAnimation()
|
||||
}
|
||||
```
|
||||
|
||||
### Spring Animation
|
||||
|
||||
```swift
|
||||
UIView.animate(
|
||||
withDuration: 0.6,
|
||||
delay: 0,
|
||||
usingSpringWithDamping: 0.7,
|
||||
initialSpringVelocity: 0.5,
|
||||
options: []
|
||||
) {
|
||||
self.popupView.transform = .identity
|
||||
}
|
||||
```
|
||||
|
||||
### Keyframe Animation
|
||||
|
||||
```swift
|
||||
UIView.animateKeyframes(withDuration: 1.0, delay: 0) {
|
||||
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.25) {
|
||||
self.view.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
|
||||
}
|
||||
UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.25) {
|
||||
self.view.transform = CGAffineTransform(rotationAngle: .pi / 4)
|
||||
}
|
||||
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
|
||||
self.view.transform = .identity
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Timing Functions
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `.linear` | Constant speed |
|
||||
| `.easeIn` | Slow start |
|
||||
| `.easeOut` | Slow end |
|
||||
| `.easeInEaseOut` | Slow start and end |
|
||||
| `.default` | System default |
|
||||
|
||||
---
|
||||
|
||||
*UIKit, Core Animation, and Apple are trademarks of Apple Inc.*
|
||||
199
ios-application-dev/references/layout-system.md
Normal file
199
ios-application-dev/references/layout-system.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Layout System
|
||||
|
||||
iOS layout system guide covering touch targets, safe areas, UICollectionView, and Compositional Layout.
|
||||
|
||||
## Touch Targets
|
||||
|
||||
Interactive elements need adequate tap areas. The recommended minimum is 44x44 points.
|
||||
|
||||
```swift
|
||||
let actionButton = UIButton(type: .system)
|
||||
actionButton.setTitle("Submit", for: .normal)
|
||||
view.addSubview(actionButton)
|
||||
|
||||
actionButton.snp.makeConstraints { make in
|
||||
make.height.greaterThanOrEqualTo(44)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-16)
|
||||
}
|
||||
```
|
||||
|
||||
Use 8-point increments for spacing (8, 16, 24, 32, 40, 48) to maintain visual consistency.
|
||||
|
||||
## Safe Area
|
||||
|
||||
Always constrain content to the safe area to avoid the notch, Dynamic Island, and home indicator.
|
||||
|
||||
```swift
|
||||
class MainViewController: UIViewController {
|
||||
private let contentStack = UIStackView()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 16
|
||||
view.addSubview(contentStack)
|
||||
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.top.bottom.equalTo(view.safeAreaLayoutGuide)
|
||||
make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(16)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## UICollectionView with Diffable Data Source
|
||||
|
||||
```swift
|
||||
class ItemsViewController: UIViewController {
|
||||
enum Section { case main }
|
||||
|
||||
private var collectionView: UICollectionView!
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupCollectionView()
|
||||
configureDataSource()
|
||||
}
|
||||
|
||||
private func setupCollectionView() {
|
||||
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
|
||||
config.trailingSwipeActionsConfigurationProvider = { [weak self] indexPath in
|
||||
self?.makeSwipeActions(for: indexPath)
|
||||
}
|
||||
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
|
||||
view.addSubview(collectionView)
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func configureDataSource() {
|
||||
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> {
|
||||
cell, indexPath, item in
|
||||
var content = cell.defaultContentConfiguration()
|
||||
content.text = item.title
|
||||
content.secondaryText = item.subtitle
|
||||
cell.contentConfiguration = content
|
||||
}
|
||||
|
||||
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
|
||||
collectionView, indexPath, item in
|
||||
collectionView.dequeueConfiguredReusableCell(
|
||||
using: cellRegistration, for: indexPath, item: item
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func updateItems(_ items: [Item]) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(items)
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Grid Layout
|
||||
|
||||
```swift
|
||||
private func createGridLayout() -> UICollectionViewLayout {
|
||||
let itemSize = NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1/3),
|
||||
heightDimension: .fractionalHeight(1.0)
|
||||
)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
item.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2)
|
||||
|
||||
let groupSize = NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1.0),
|
||||
heightDimension: .fractionalWidth(1/3)
|
||||
)
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
return UICollectionViewCompositionalLayout(section: section)
|
||||
}
|
||||
```
|
||||
|
||||
## Sectioned List with Headers
|
||||
|
||||
```swift
|
||||
class CategorizedListVC: UIViewController {
|
||||
enum Section: Hashable {
|
||||
case favorites, recent, all
|
||||
}
|
||||
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
private func setupCollectionView() {
|
||||
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
|
||||
config.headerMode = .supplementary
|
||||
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
}
|
||||
|
||||
private func configureDataSource() {
|
||||
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> {
|
||||
cell, indexPath, item in
|
||||
var content = cell.defaultContentConfiguration()
|
||||
content.text = item.title
|
||||
cell.contentConfiguration = content
|
||||
}
|
||||
|
||||
let headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(
|
||||
elementKind: UICollectionView.elementKindSectionHeader
|
||||
) { [weak self] header, elementKind, indexPath in
|
||||
guard let section = self?.dataSource.sectionIdentifier(for: indexPath.section) else { return }
|
||||
var content = header.defaultContentConfiguration()
|
||||
content.text = self?.title(for: section)
|
||||
header.contentConfiguration = content
|
||||
}
|
||||
|
||||
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
|
||||
collectionView, indexPath, item in
|
||||
collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
|
||||
}
|
||||
|
||||
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
|
||||
collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
func applySnapshot(favorites: [Item], recent: [Item], all: [Item]) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
if !favorites.isEmpty {
|
||||
snapshot.appendSections([.favorites])
|
||||
snapshot.appendItems(favorites, toSection: .favorites)
|
||||
}
|
||||
if !recent.isEmpty {
|
||||
snapshot.appendSections([.recent])
|
||||
snapshot.appendItems(recent, toSection: .recent)
|
||||
}
|
||||
snapshot.appendSections([.all])
|
||||
snapshot.appendItems(all, toSection: .all)
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Spacing Guidelines
|
||||
|
||||
| Spacing | Usage |
|
||||
|---------|-------|
|
||||
| 8pt | Compact element spacing |
|
||||
| 16pt | Standard padding |
|
||||
| 24pt | Section spacing |
|
||||
| 32pt | Large section separation |
|
||||
| 48pt | Screen margins (large screens) |
|
||||
|
||||
---
|
||||
|
||||
*UIKit and Apple are trademarks of Apple Inc. SnapKit is a trademark of its respective owners.*
|
||||
178
ios-application-dev/references/metal-shader.md
Normal file
178
ios-application-dev/references/metal-shader.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# Metal Shader Reference
|
||||
|
||||
Expert reference for Metal shaders, real-time rendering, and Apple's Tile-Based Deferred Rendering (TBDR) architecture.
|
||||
|
||||
## Core Principles
|
||||
|
||||
**Half precision first → Leverage TBDR → Function constant specialization → Use Intersector API**
|
||||
|
||||
### When to Use
|
||||
|
||||
- Metal Shading Language (MSL) development
|
||||
- Apple GPU optimization (TBDR architecture)
|
||||
- PBR rendering pipelines
|
||||
- Compute shaders and parallel processing
|
||||
- Apple Silicon ray tracing
|
||||
- GPU profiling and debugging
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
- WebGL/GLSL (different architecture)
|
||||
- CUDA (NVIDIA only)
|
||||
- OpenGL (deprecated on Apple)
|
||||
- CPU-side optimization
|
||||
|
||||
## Expert vs Novice
|
||||
|
||||
| Topic | Novice | Expert |
|
||||
|-------|--------|--------|
|
||||
| Data types | `float` everywhere | Default `half`, `float` only for position/depth |
|
||||
| Branching | Runtime conditionals | Function constants for compile-time elimination |
|
||||
| Memory | Everything in device | Know constant/device/threadgroup tradeoffs |
|
||||
| Architecture | Treat as desktop GPU | Understand TBDR: tile memory is free, bandwidth is expensive |
|
||||
| Ray tracing | intersection queries | intersector API (hardware-aligned) |
|
||||
| Debugging | print debugging | GPU capture, shader profiler, occupancy analysis |
|
||||
|
||||
## Common Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Problem | Solution |
|
||||
|--------------|---------|----------|
|
||||
| 32-bit floats | Wastes registers, reduces occupancy, doubles bandwidth | Default `half`, `float` only for position/depth |
|
||||
| Ignoring TBDR | Not using free tile memory | Use `[[color(n)]]`, memoryless targets |
|
||||
| Runtime constant branches | Warp divergence, wastes ALU | Function constants + pipeline specialization |
|
||||
| intersection queries | Not hardware-aligned | Use intersector API |
|
||||
|
||||
## Metal Evolution
|
||||
|
||||
| Era | Key Development |
|
||||
|-----|-----------------|
|
||||
| Metal 2.x | OpenGL migration, basic compute |
|
||||
| Apple Silicon | Unified memory, tile shaders critical |
|
||||
| Metal 3 | Mesh shaders, hardware-accelerated ray tracing |
|
||||
| Latest | Neural Engine + GPU cooperation, Vision Pro foveated rendering |
|
||||
|
||||
**Apple Family 9 Note**: Threadgroup memory less advantageous vs direct device access.
|
||||
|
||||
## Shader Types
|
||||
|
||||
| Type | Purpose | Key Attributes |
|
||||
|------|---------|----------------|
|
||||
| Vertex | Vertex transformation | `[[stage_in]]`, `[[buffer(n)]]` |
|
||||
| Fragment | Pixel shading | `[[color(n)]]`, `[[texture(n)]]` |
|
||||
| Compute/Kernel | General computation | `[[thread_position_in_grid]]` |
|
||||
| Tile | TBDR-specific | `[[imageblock]]` |
|
||||
| Mesh | Metal 3 geometry | `[[mesh_id]]` |
|
||||
|
||||
## Rendering Techniques
|
||||
|
||||
| Technique | Description |
|
||||
|-----------|-------------|
|
||||
| Fullscreen quad | 4 vertex triangle strip, no MVP, post-processing basis |
|
||||
| PBR Cook-Torrance | Fresnel Schlick + GGX Distribution + Smith Geometry |
|
||||
| Blinn-Phong | Simple specular, half-vector calculation |
|
||||
|
||||
## Procedural Generation
|
||||
|
||||
| Technique | Use Case |
|
||||
|-----------|----------|
|
||||
| Hash functions | Pseudo-random basis for noise, random sampling |
|
||||
| Voronoi | Cell textures, stones, cracks |
|
||||
| Value/Perlin Noise | Continuous random fields |
|
||||
| FBM | Multi-octave layering, fractal terrain, clouds |
|
||||
| Domain Warping | Coordinate distortion, organic shapes |
|
||||
|
||||
## Numerical Techniques
|
||||
|
||||
| Technique | Formula |
|
||||
|-----------|---------|
|
||||
| Central difference gradient | `(f(x+h) - f(x-h)) / (2h)` |
|
||||
| Smoothstep | `x * x * (3 - 2 * x)` |
|
||||
| SDF operations | `min/max/smooth_min` boolean ops |
|
||||
|
||||
## SwiftUI + MTKView Integration
|
||||
|
||||
### Architecture Pattern
|
||||
|
||||
```
|
||||
MetalView (UIViewRepresentable)
|
||||
└── Coordinator = Renderer (MTKViewDelegate)
|
||||
├── MTLDevice
|
||||
├── MTLCommandQueue
|
||||
├── MTLRenderPipelineState
|
||||
└── MTLBuffer (vertices, uniforms)
|
||||
```
|
||||
|
||||
### Uniform Alignment Rules
|
||||
|
||||
| Swift Type | Metal Type | Alignment |
|
||||
|------------|------------|-----------|
|
||||
| `Float` | `float` | 4 bytes |
|
||||
| `SIMD2<Float>` | `float2` | 8 bytes |
|
||||
| `SIMD3<Float>` | `float3` | **16 bytes** |
|
||||
| `SIMD4<Float>` | `float4` | 16 bytes |
|
||||
|
||||
**Key**: `float3` aligns to 16 bytes. Use `MemoryLayout<T>.size` to verify.
|
||||
|
||||
## Command Line Tools
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `xcrun metal -c shader.metal -o shader.air` | Compile to AIR |
|
||||
| `xcrun metallib shader.air -o shader.metallib` | Link to metallib |
|
||||
| `xcrun metal shader.metal -o shader.metallib` | One-step compile & link |
|
||||
| `xcrun metal -Weverything -c shader.metal` | Syntax check |
|
||||
| `xcrun metal-objdump --disassemble shader.metallib` | Disassemble |
|
||||
|
||||
## GPU Debugging
|
||||
|
||||
### Xcode Workflow
|
||||
|
||||
1. **GPU Capture**: ⌘⇧⌥G
|
||||
2. **Shader Profiler**: Select draw call → View Shader
|
||||
3. **Memory Viewer**: Inspect buffer/texture
|
||||
4. **Performance HUD**: Enable in device options
|
||||
|
||||
### Key Metrics
|
||||
|
||||
| Metric | Healthy Value | Low Value Cause |
|
||||
|--------|---------------|-----------------|
|
||||
| GPU Occupancy | > 80% | Memory bandwidth bottleneck |
|
||||
| ALU Utilization | > 60% | Waiting on memory |
|
||||
| Bandwidth | As low as possible | TBDR should minimize store |
|
||||
|
||||
### Debug Utility Functions
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| heatmap | Value visualization (blue→green→red) |
|
||||
| debugNaN | NaN/Inf detection (magenta marker) |
|
||||
| visualizeDepth | Linearized depth visualization |
|
||||
|
||||
## Performance Optimization Checklist
|
||||
|
||||
### Data Types
|
||||
- [ ] Default `half`, `float` only for position/depth
|
||||
|
||||
### Memory Management
|
||||
- [ ] Constants in constant address space
|
||||
- [ ] Use `.storageModeShared`
|
||||
- [ ] Leverage tile memory (TBDR free reads)
|
||||
- [ ] Avoid unnecessary render target stores
|
||||
|
||||
### Branch Optimization
|
||||
- [ ] Function constants to eliminate branches
|
||||
- [ ] Fixed loop bounds (GPU unrolling)
|
||||
|
||||
### Rendering Tips
|
||||
- [ ] Fullscreen quad with 4 vertex triangle strip
|
||||
- [ ] Procedural textures to avoid sampling bandwidth
|
||||
- [ ] `[[early_fragment_tests]]` for early depth test
|
||||
- [ ] `setFragmentBytes` for small data
|
||||
|
||||
### Compute Optimization
|
||||
- [ ] Vectorize (SIMD)
|
||||
- [ ] Reduce register pressure
|
||||
|
||||
---
|
||||
|
||||
*Metal, Apple Silicon, and Xcode are trademarks of Apple Inc.*
|
||||
175
ios-application-dev/references/navigation-patterns.md
Normal file
175
ios-application-dev/references/navigation-patterns.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Navigation Patterns
|
||||
|
||||
iOS navigation patterns guide covering Tab navigation, Navigation Controller, and modal presentation.
|
||||
|
||||
## Tab-Based Navigation
|
||||
|
||||
For apps with 3-5 main sections:
|
||||
|
||||
```swift
|
||||
class AppTabBarController: UITabBarController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let homeNav = UINavigationController(rootViewController: HomeVC())
|
||||
homeNav.tabBarItem = UITabBarItem(
|
||||
title: "Home",
|
||||
image: UIImage(systemName: "house"),
|
||||
selectedImage: UIImage(systemName: "house.fill")
|
||||
)
|
||||
|
||||
let searchNav = UINavigationController(rootViewController: SearchVC())
|
||||
searchNav.tabBarItem = UITabBarItem(
|
||||
title: "Search",
|
||||
image: UIImage(systemName: "magnifyingglass"),
|
||||
tag: 1
|
||||
)
|
||||
|
||||
let profileNav = UINavigationController(rootViewController: ProfileVC())
|
||||
profileNav.tabBarItem = UITabBarItem(
|
||||
title: "Profile",
|
||||
image: UIImage(systemName: "person"),
|
||||
selectedImage: UIImage(systemName: "person.fill")
|
||||
)
|
||||
|
||||
viewControllers = [homeNav, searchNav, profileNav]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tab Bar Best Practices
|
||||
|
||||
| Principle | Description |
|
||||
|-----------|-------------|
|
||||
| Limit count | Maximum 5 tabs, use More for additional |
|
||||
| Always visible | Tab bar stays visible at all navigation levels |
|
||||
| State preservation | Preserve navigation state when switching tabs |
|
||||
| Icon choice | Use SF Symbols, provide selected/unselected states |
|
||||
|
||||
## Navigation Controller
|
||||
|
||||
Use large titles for root views:
|
||||
|
||||
```swift
|
||||
class ListViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "Items"
|
||||
navigationController?.navigationBar.prefersLargeTitles = true
|
||||
navigationItem.largeTitleDisplayMode = .always
|
||||
}
|
||||
|
||||
func pushDetail(_ item: Item) {
|
||||
let detail = DetailViewController(item: item)
|
||||
detail.navigationItem.largeTitleDisplayMode = .never
|
||||
navigationController?.pushViewController(detail, animated: true)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Navigation Bar Configuration
|
||||
|
||||
```swift
|
||||
class CustomNavigationController: UINavigationController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let appearance = UINavigationBarAppearance()
|
||||
appearance.configureWithDefaultBackground()
|
||||
|
||||
navigationBar.standardAppearance = appearance
|
||||
navigationBar.scrollEdgeAppearance = appearance
|
||||
navigationBar.compactAppearance = appearance
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Navigation Bar Buttons
|
||||
|
||||
```swift
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
image: UIImage(systemName: "plus"),
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(addItem)
|
||||
)
|
||||
|
||||
navigationItem.rightBarButtonItems = [
|
||||
UIBarButtonItem(systemItem: .add, primaryAction: UIAction { _ in }),
|
||||
UIBarButtonItem(systemItem: .edit, primaryAction: UIAction { _ in })
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Modal Presentation
|
||||
|
||||
### Sheet Presentation
|
||||
|
||||
```swift
|
||||
func presentEditor() {
|
||||
let editorVC = EditorViewController()
|
||||
let nav = UINavigationController(rootViewController: editorVC)
|
||||
|
||||
editorVC.navigationItem.leftBarButtonItem = UIBarButtonItem(
|
||||
systemItem: .cancel, target: self, action: #selector(dismissEditor)
|
||||
)
|
||||
editorVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
systemItem: .done, target: self, action: #selector(saveAndDismiss)
|
||||
)
|
||||
|
||||
if let sheet = nav.sheetPresentationController {
|
||||
sheet.detents = [.medium(), .large()]
|
||||
sheet.prefersGrabberVisible = true
|
||||
sheet.prefersScrollingExpandsWhenScrolledToEdge = false
|
||||
}
|
||||
|
||||
present(nav, animated: true)
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Detent (iOS 16+)
|
||||
|
||||
```swift
|
||||
if let sheet = nav.sheetPresentationController {
|
||||
let customDetent = UISheetPresentationController.Detent.custom { context in
|
||||
return context.maximumDetentValue * 0.4
|
||||
}
|
||||
sheet.detents = [customDetent, .large()]
|
||||
}
|
||||
```
|
||||
|
||||
### Full Screen Presentation
|
||||
|
||||
```swift
|
||||
func presentFullScreen() {
|
||||
let vc = FullScreenViewController()
|
||||
vc.modalPresentationStyle = .fullScreen
|
||||
vc.modalTransitionStyle = .coverVertical
|
||||
present(vc, animated: true)
|
||||
}
|
||||
```
|
||||
|
||||
## Presentation Styles
|
||||
|
||||
| Style | Usage |
|
||||
|-------|-------|
|
||||
| `.automatic` | System default (usually sheet) |
|
||||
| `.pageSheet` | Card-style, parent view visible |
|
||||
| `.fullScreen` | Full screen cover |
|
||||
| `.overFullScreen` | Full screen with transparent background |
|
||||
| `.popover` | iPad popover |
|
||||
|
||||
## Navigation Best Practices
|
||||
|
||||
1. **Back gesture** - Ensure edge swipe back always works
|
||||
2. **State restoration** - Use `UIStateRestoring` to save navigation stack
|
||||
3. **Depth limit** - Avoid more than 4-5 navigation levels
|
||||
4. **Cancel button** - Modal views must provide a cancel option
|
||||
5. **Save confirmation** - Show confirmation dialog for unsaved changes
|
||||
|
||||
---
|
||||
|
||||
*UIKit, SF Symbols, and Apple are trademarks of Apple Inc.*
|
||||
741
ios-application-dev/references/swift-coding-standards.md
Normal file
741
ios-application-dev/references/swift-coding-standards.md
Normal file
@@ -0,0 +1,741 @@
|
||||
# Swift Coding Standards
|
||||
|
||||
Best practices for writing clean, safe, and idiomatic Swift code following Apple's guidelines and modern Swift conventions.
|
||||
|
||||
---
|
||||
|
||||
## 1. Optionals and Safety
|
||||
|
||||
**Impact:** CRITICAL
|
||||
|
||||
Swift's optional system eliminates null pointer exceptions through compile-time safety.
|
||||
|
||||
### 1.1 Safe Unwrapping with if let
|
||||
|
||||
```swift
|
||||
if let name = optionalName {
|
||||
print("Hello, \(name)")
|
||||
}
|
||||
|
||||
// Multiple bindings
|
||||
if let name = userName, let age = userAge, age >= 18 {
|
||||
print("\(name) is an adult")
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Guard for Early Exit
|
||||
|
||||
Use `guard` to exit early when preconditions aren't met:
|
||||
|
||||
```swift
|
||||
func processUser(_ user: User?) {
|
||||
guard let user = user else { return }
|
||||
guard !user.name.isEmpty else { return }
|
||||
print(user.name)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 Nil Coalescing for Defaults
|
||||
|
||||
```swift
|
||||
let displayName = name ?? "Anonymous"
|
||||
let count = items?.count ?? 0
|
||||
```
|
||||
|
||||
### 1.4 Optional Chaining
|
||||
|
||||
```swift
|
||||
let count = user?.profile?.posts?.count
|
||||
let uppercased = optionalString?.uppercased()
|
||||
```
|
||||
|
||||
### 1.5 Optional map/flatMap
|
||||
|
||||
```swift
|
||||
let uppercasedName = userName.map { $0.uppercased() }
|
||||
let userID = userIDString.flatMap { Int($0) }
|
||||
```
|
||||
|
||||
### 1.6 Never Force Unwrap
|
||||
|
||||
Avoid `!` force unwrapping. Use safe alternatives:
|
||||
|
||||
| Instead of | Use |
|
||||
|------------|-----|
|
||||
| `value!` | `if let value = value { }` |
|
||||
| `array[0]` (unsafe) | `array.first` |
|
||||
| `dictionary["key"]!` | `dictionary["key", default: defaultValue]` |
|
||||
|
||||
---
|
||||
|
||||
## 2. Naming Conventions
|
||||
|
||||
**Impact:** HIGH
|
||||
|
||||
### 2.1 Types: PascalCase
|
||||
|
||||
```swift
|
||||
class UserProfileViewController { }
|
||||
struct NetworkRequest { }
|
||||
protocol DataSource { }
|
||||
enum LoadingState { }
|
||||
```
|
||||
|
||||
### 2.2 Variables and Functions: camelCase
|
||||
|
||||
```swift
|
||||
var userName: String
|
||||
let maximumRetryCount = 3
|
||||
func fetchUserProfile() { }
|
||||
```
|
||||
|
||||
### 2.3 Boolean Naming
|
||||
|
||||
Use `is`, `has`, `should`, `can` prefixes:
|
||||
|
||||
```swift
|
||||
var isLoading: Bool
|
||||
var hasCompletedOnboarding: Bool
|
||||
var shouldShowAlert: Bool
|
||||
var canEditProfile: Bool
|
||||
```
|
||||
|
||||
### 2.4 Function Naming
|
||||
|
||||
Use verb phrases, read like natural English:
|
||||
|
||||
```swift
|
||||
// Good - clear actions
|
||||
func fetchUsers() async throws -> [User]
|
||||
func remove(_ item: Item, at index: Int)
|
||||
func makeIterator() -> Iterator
|
||||
|
||||
// Avoid - unclear or redundant
|
||||
func getUsersData() // "get" is redundant
|
||||
func doRemove() // vague
|
||||
```
|
||||
|
||||
### 2.5 Parameter Labels
|
||||
|
||||
First parameter label can be omitted when obvious:
|
||||
|
||||
```swift
|
||||
func insert(_ element: Element, at index: Int)
|
||||
func move(from source: Int, to destination: Int)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Protocol-Oriented Design
|
||||
|
||||
**Impact:** HIGH
|
||||
|
||||
Swift favors composition over inheritance through protocols.
|
||||
|
||||
### 3.1 Define Capabilities Through Protocols
|
||||
|
||||
```swift
|
||||
protocol DataStore {
|
||||
func save<T: Codable>(_ item: T, key: String) throws
|
||||
func load<T: Codable>(key: String) throws -> T?
|
||||
}
|
||||
|
||||
protocol Drawable {
|
||||
var color: Color { get set }
|
||||
func draw()
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Protocol Extensions for Default Behavior
|
||||
|
||||
```swift
|
||||
extension Drawable {
|
||||
func draw() {
|
||||
print("Drawing with \(color)")
|
||||
}
|
||||
}
|
||||
|
||||
extension Collection {
|
||||
func chunked(into size: Int) -> [[Element]] {
|
||||
stride(from: 0, to: count, by: size).map {
|
||||
Array(self[$0..<Swift.min($0 + size, count)])
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Associated Types for Flexibility
|
||||
|
||||
```swift
|
||||
protocol Repository {
|
||||
associatedtype Item
|
||||
func fetchAll() async throws -> [Item]
|
||||
func save(_ item: Item) async throws
|
||||
}
|
||||
|
||||
class UserRepository: Repository {
|
||||
typealias Item = User
|
||||
|
||||
func fetchAll() async throws -> [User] { /* ... */ }
|
||||
func save(_ item: User) async throws { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Protocol Composition
|
||||
|
||||
```swift
|
||||
protocol Named { var name: String { get } }
|
||||
protocol Aged { var age: Int { get } }
|
||||
|
||||
func greet(_ person: Named & Aged) {
|
||||
print("Hello, \(person.name), age \(person.age)")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Value Types vs Reference Types
|
||||
|
||||
**Impact:** HIGH
|
||||
|
||||
### 4.1 Prefer Structs (Value Types)
|
||||
|
||||
Use structs for simple data models, independent copies:
|
||||
|
||||
```swift
|
||||
struct User {
|
||||
var name: String
|
||||
var email: String
|
||||
}
|
||||
|
||||
struct Point {
|
||||
var x: Double
|
||||
var y: Double
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Use Classes When Needed
|
||||
|
||||
Use classes for shared mutable state, identity matters:
|
||||
|
||||
```swift
|
||||
class NetworkManager {
|
||||
static let shared = NetworkManager()
|
||||
private init() { }
|
||||
}
|
||||
|
||||
class FileHandle {
|
||||
// Wrapping system resource
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Enums for Finite States
|
||||
|
||||
```swift
|
||||
enum LoadingState {
|
||||
case idle
|
||||
case loading
|
||||
case success(Data)
|
||||
case failure(Error)
|
||||
}
|
||||
|
||||
enum Result<Success, Failure: Error> {
|
||||
case success(Success)
|
||||
case failure(Failure)
|
||||
}
|
||||
```
|
||||
|
||||
| Type | Use When |
|
||||
|------|----------|
|
||||
| `struct` | Data models, coordinates, independent values |
|
||||
| `class` | Shared state, identity matters, inheritance needed |
|
||||
| `enum` | Finite set of options, state machines |
|
||||
|
||||
---
|
||||
|
||||
## 5. Memory Management with ARC
|
||||
|
||||
**Impact:** CRITICAL
|
||||
|
||||
### 5.1 Breaking Retain Cycles with weak
|
||||
|
||||
```swift
|
||||
class Apartment {
|
||||
weak var tenant: Person?
|
||||
}
|
||||
|
||||
class Person {
|
||||
var apartment: Apartment?
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Closure Capture Lists
|
||||
|
||||
```swift
|
||||
// Weak capture for optional self
|
||||
onComplete = { [weak self] in
|
||||
self?.processResult()
|
||||
}
|
||||
|
||||
// Capture specific values
|
||||
let id = user.id
|
||||
fetchData { [id] result in
|
||||
print("Fetched for \(id)")
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 unowned for Guaranteed Lifetime
|
||||
|
||||
Use when reference should never be nil during object lifetime:
|
||||
|
||||
```swift
|
||||
class CreditCard {
|
||||
unowned let customer: Customer
|
||||
|
||||
init(customer: Customer) {
|
||||
self.customer = customer
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Keyword | Use When |
|
||||
|---------|----------|
|
||||
| `weak` | Reference may become nil |
|
||||
| `unowned` | Reference guaranteed to outlive |
|
||||
| None | Strong ownership needed |
|
||||
|
||||
---
|
||||
|
||||
## 6. Error Handling
|
||||
|
||||
**Impact:** HIGH
|
||||
|
||||
### 6.1 Define Typed Errors
|
||||
|
||||
```swift
|
||||
enum NetworkError: Error {
|
||||
case invalidURL
|
||||
case noConnection
|
||||
case serverError(statusCode: Int)
|
||||
case decodingFailed(underlying: Error)
|
||||
}
|
||||
|
||||
enum ValidationError: LocalizedError {
|
||||
case emptyField(name: String)
|
||||
case invalidFormat(field: String, expected: String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .emptyField(let name):
|
||||
return "\(name) cannot be empty"
|
||||
case .invalidFormat(let field, let expected):
|
||||
return "\(field) must be \(expected)"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Throwing Functions
|
||||
|
||||
```swift
|
||||
func fetchUser(id: Int) throws -> User {
|
||||
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
|
||||
throw NetworkError.invalidURL
|
||||
}
|
||||
// ... implementation
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Do-Catch Handling
|
||||
|
||||
```swift
|
||||
do {
|
||||
let user = try fetchUser(id: 123)
|
||||
print(user.name)
|
||||
} catch NetworkError.serverError(let code) {
|
||||
print("Server error: \(code)")
|
||||
} catch NetworkError.noConnection {
|
||||
print("Check your internet connection")
|
||||
} catch {
|
||||
print("Unknown error: \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 try? and try!
|
||||
|
||||
```swift
|
||||
// try? returns optional (nil on error)
|
||||
let user = try? fetchUser(id: 123)
|
||||
|
||||
// try! crashes on error - use only when failure is programmer error
|
||||
let config = try! loadBundledConfig()
|
||||
```
|
||||
|
||||
### 6.5 Rethrows
|
||||
|
||||
```swift
|
||||
func perform<T>(_ operation: () throws -> T) rethrows -> T {
|
||||
return try operation()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Modern Concurrency (async/await)
|
||||
|
||||
**Impact:** CRITICAL
|
||||
|
||||
### 7.1 Async Functions
|
||||
|
||||
```swift
|
||||
func fetchUser(id: Int) async throws -> User {
|
||||
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
|
||||
throw NetworkError.invalidURL
|
||||
}
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
return try JSONDecoder().decode(User.self, from: data)
|
||||
}
|
||||
|
||||
// Calling async functions
|
||||
Task {
|
||||
do {
|
||||
let user = try await fetchUser(id: 123)
|
||||
print(user.name)
|
||||
} catch {
|
||||
print("Failed: \(error)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Parallel Execution with TaskGroup
|
||||
|
||||
```swift
|
||||
func fetchAllUsers(ids: [Int]) async throws -> [User] {
|
||||
try await withThrowingTaskGroup(of: User.self) { group in
|
||||
for id in ids {
|
||||
group.addTask {
|
||||
try await fetchUser(id: id)
|
||||
}
|
||||
}
|
||||
return try await group.reduce(into: []) { $0.append($1) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 async let for Concurrent Bindings
|
||||
|
||||
```swift
|
||||
async let user = fetchUser(id: 1)
|
||||
async let posts = fetchPosts(userId: 1)
|
||||
async let followers = fetchFollowers(userId: 1)
|
||||
|
||||
let profile = try await ProfileData(
|
||||
user: user,
|
||||
posts: posts,
|
||||
followers: followers
|
||||
)
|
||||
```
|
||||
|
||||
### 7.4 Actors for Thread-Safe State
|
||||
|
||||
```swift
|
||||
actor BankAccount {
|
||||
private var balance: Double = 0
|
||||
|
||||
func deposit(_ amount: Double) {
|
||||
balance += amount
|
||||
}
|
||||
|
||||
func withdraw(_ amount: Double) throws {
|
||||
guard balance >= amount else {
|
||||
throw BankError.insufficientFunds
|
||||
}
|
||||
balance -= amount
|
||||
}
|
||||
|
||||
func getBalance() -> Double {
|
||||
balance
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
let account = BankAccount()
|
||||
await account.deposit(100)
|
||||
let balance = await account.getBalance()
|
||||
```
|
||||
|
||||
### 7.5 MainActor for UI Updates
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
class ViewModel: ObservableObject {
|
||||
@Published var isLoading = false
|
||||
@Published var users: [User] = []
|
||||
|
||||
func loadUsers() async {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
|
||||
do {
|
||||
users = try await fetchUsers()
|
||||
} catch {
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.6 Task Cancellation
|
||||
|
||||
```swift
|
||||
func fetchWithTimeout() async throws -> Data {
|
||||
try await withThrowingTaskGroup(of: Data.self) { group in
|
||||
group.addTask {
|
||||
try await fetchData()
|
||||
}
|
||||
group.addTask {
|
||||
try await Task.sleep(for: .seconds(10))
|
||||
throw TimeoutError()
|
||||
}
|
||||
|
||||
let result = try await group.next()!
|
||||
group.cancelAll()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Check for cancellation
|
||||
func longOperation() async throws {
|
||||
for item in items {
|
||||
try Task.checkCancellation()
|
||||
await process(item)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Access Control
|
||||
|
||||
**Impact:** MEDIUM
|
||||
|
||||
### 8.1 Access Levels
|
||||
|
||||
| Level | Scope |
|
||||
|-------|-------|
|
||||
| `private` | Enclosing declaration only |
|
||||
| `fileprivate` | Entire source file |
|
||||
| `internal` | Module (default) |
|
||||
| `public` | Other modules can access |
|
||||
| `open` | Other modules can subclass/override |
|
||||
|
||||
### 8.2 Best Practices
|
||||
|
||||
```swift
|
||||
public class UserService {
|
||||
// Public API
|
||||
public func fetchUser(id: Int) async throws -> User { }
|
||||
|
||||
// Internal helper
|
||||
func buildRequest(for id: Int) -> URLRequest { }
|
||||
|
||||
// Private implementation detail
|
||||
private let session: URLSession
|
||||
private var cache: [Int: User] = [:]
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 Private Setters
|
||||
|
||||
```swift
|
||||
public struct Counter {
|
||||
public private(set) var count = 0
|
||||
|
||||
public mutating func increment() {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Generics and Type Constraints
|
||||
|
||||
**Impact:** MEDIUM
|
||||
|
||||
### 9.1 Generic Functions
|
||||
|
||||
```swift
|
||||
func swapValues<T>(_ a: inout T, _ b: inout T) {
|
||||
let temp = a
|
||||
a = b
|
||||
b = temp
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 Type Constraints
|
||||
|
||||
```swift
|
||||
func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
|
||||
array.firstIndex(of: value)
|
||||
}
|
||||
|
||||
func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
|
||||
try JSONDecoder().decode(type, from: data)
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 Where Clauses
|
||||
|
||||
```swift
|
||||
func allMatch<C: Collection>(_ collection: C, predicate: (C.Element) -> Bool) -> Bool
|
||||
where C.Element: Equatable {
|
||||
collection.allSatisfy(predicate)
|
||||
}
|
||||
|
||||
extension Array where Element: Numeric {
|
||||
func sum() -> Element {
|
||||
reduce(0, +)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.4 Opaque Types (some)
|
||||
|
||||
```swift
|
||||
func makeCollection() -> some Collection {
|
||||
[1, 2, 3]
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text("Hello")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Property Wrappers
|
||||
|
||||
**Impact:** MEDIUM
|
||||
|
||||
### 10.1 Common SwiftUI Property Wrappers
|
||||
|
||||
| Wrapper | Use Case |
|
||||
|---------|----------|
|
||||
| `@State` | View-local mutable state |
|
||||
| `@Binding` | Two-way connection to parent state |
|
||||
| `@StateObject` | View-owned observable object |
|
||||
| `@ObservedObject` | Passed-in observable object |
|
||||
| `@EnvironmentObject` | Shared object from ancestor |
|
||||
| `@Environment` | System environment values |
|
||||
| `@Published` | Observable property in class |
|
||||
|
||||
### 10.2 Custom Property Wrappers
|
||||
|
||||
```swift
|
||||
@propertyWrapper
|
||||
struct Clamped<Value: Comparable> {
|
||||
private var value: Value
|
||||
let range: ClosedRange<Value>
|
||||
|
||||
var wrappedValue: Value {
|
||||
get { value }
|
||||
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
|
||||
}
|
||||
|
||||
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
|
||||
self.range = range
|
||||
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
|
||||
}
|
||||
}
|
||||
|
||||
struct Settings {
|
||||
@Clamped(0...100) var volume: Int = 50
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Optionals
|
||||
|
||||
```swift
|
||||
if let x = optional { } // Safe unwrap
|
||||
guard let x = optional else { return } // Early exit
|
||||
let x = optional ?? default // Default value
|
||||
optional?.method() // Optional chaining
|
||||
optional.map { transform($0) } // Transform if present
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
```swift
|
||||
// Defer for cleanup
|
||||
func process() {
|
||||
let file = openFile()
|
||||
defer { closeFile(file) }
|
||||
// ... work with file
|
||||
}
|
||||
|
||||
// Lazy initialization
|
||||
lazy var expensive: ExpensiveObject = {
|
||||
ExpensiveObject()
|
||||
}()
|
||||
|
||||
// Type inference
|
||||
let numbers = [1, 2, 3] // [Int]
|
||||
let doubled = numbers.map { $0 * 2 } // [Int]
|
||||
```
|
||||
|
||||
### Closure Syntax
|
||||
|
||||
```swift
|
||||
// Full syntax
|
||||
let sorted = names.sorted(by: { (s1: String, s2: String) -> Bool in
|
||||
return s1 < s2
|
||||
})
|
||||
|
||||
// Shortened
|
||||
let sorted = names.sorted { $0 < $1 }
|
||||
|
||||
// Trailing closure
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
view.alpha = 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
### Safety
|
||||
- [ ] No force unwrapping (`!`) except for IB outlets and known-safe cases
|
||||
- [ ] All optionals handled with `if let`, `guard let`, or `??`
|
||||
- [ ] No implicitly unwrapped optionals (`!`) in data models
|
||||
|
||||
### Memory
|
||||
- [ ] Closures use `[weak self]` when capturing self in escaping closures
|
||||
- [ ] Delegate properties are `weak`
|
||||
- [ ] No retain cycles between objects
|
||||
|
||||
### Concurrency
|
||||
- [ ] Async functions used instead of completion handlers
|
||||
- [ ] Actors protect shared mutable state
|
||||
- [ ] UI updates on `@MainActor`
|
||||
- [ ] Task cancellation checked in long operations
|
||||
|
||||
### Access Control
|
||||
- [ ] `private` used for implementation details
|
||||
- [ ] `public` API is minimal and intentional
|
||||
- [ ] No unnecessary `internal` exposure
|
||||
|
||||
### Naming
|
||||
- [ ] Types use PascalCase
|
||||
- [ ] Functions and variables use camelCase
|
||||
- [ ] Booleans have `is`/`has`/`should` prefix
|
||||
- [ ] Functions read like natural English
|
||||
|
||||
---
|
||||
|
||||
*Swift and Apple are trademarks of Apple Inc.*
|
||||
1167
ios-application-dev/references/swiftui-design-guidelines.md
Normal file
1167
ios-application-dev/references/swiftui-design-guidelines.md
Normal file
File diff suppressed because it is too large
Load Diff
401
ios-application-dev/references/system-integration.md
Normal file
401
ios-application-dev/references/system-integration.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# System Integration
|
||||
|
||||
iOS system integration guide covering permissions, location, sharing, app lifecycle, and haptic feedback.
|
||||
|
||||
## Permission Requests
|
||||
|
||||
Request permissions contextually, not at launch:
|
||||
|
||||
```swift
|
||||
import AVFoundation
|
||||
|
||||
@objc func openCamera() {
|
||||
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
||||
DispatchQueue.main.async {
|
||||
if granted {
|
||||
self?.showCameraInterface()
|
||||
} else {
|
||||
self?.showPermissionDeniedAlert()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Photo Library
|
||||
|
||||
```swift
|
||||
import Photos
|
||||
|
||||
func requestPhotoAccess() {
|
||||
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
|
||||
DispatchQueue.main.async {
|
||||
switch status {
|
||||
case .authorized, .limited:
|
||||
self.showPhotoPicker()
|
||||
case .denied, .restricted:
|
||||
self.showSettingsAlert()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Microphone
|
||||
|
||||
```swift
|
||||
func requestMicrophoneAccess() {
|
||||
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
||||
DispatchQueue.main.async {
|
||||
if granted {
|
||||
self.startRecording()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Notifications
|
||||
|
||||
```swift
|
||||
import UserNotifications
|
||||
|
||||
func requestNotificationPermission() {
|
||||
UNUserNotificationCenter.current().requestAuthorization(
|
||||
options: [.alert, .badge, .sound]
|
||||
) { granted, error in
|
||||
DispatchQueue.main.async {
|
||||
if granted {
|
||||
self.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Location Button
|
||||
|
||||
For one-time location access without persistent permission:
|
||||
|
||||
```swift
|
||||
import CoreLocationUI
|
||||
|
||||
class StoreFinderVC: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let locationBtn = CLLocationButton()
|
||||
locationBtn.icon = .arrowFilled
|
||||
locationBtn.label = .currentLocation
|
||||
locationBtn.cornerRadius = 20
|
||||
locationBtn.addTarget(self, action: #selector(findNearby), for: .touchUpInside)
|
||||
|
||||
view.addSubview(locationBtn)
|
||||
locationBtn.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-24)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Core Location
|
||||
|
||||
```swift
|
||||
import CoreLocation
|
||||
|
||||
class LocationManager: NSObject, CLLocationManagerDelegate {
|
||||
private let manager = CLLocationManager()
|
||||
|
||||
func requestLocation() {
|
||||
manager.delegate = self
|
||||
manager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
manager.requestWhenInUseAuthorization()
|
||||
}
|
||||
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
switch manager.authorizationStatus {
|
||||
case .authorizedWhenInUse, .authorizedAlways:
|
||||
manager.requestLocation()
|
||||
case .denied:
|
||||
showLocationDeniedAlert()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.last else { return }
|
||||
handleLocation(location)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Share Sheet
|
||||
|
||||
```swift
|
||||
@objc func shareContent() {
|
||||
let items: [Any] = [contentURL, contentImage].compactMap { $0 }
|
||||
let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil)
|
||||
|
||||
if let popover = activityVC.popoverPresentationController {
|
||||
popover.sourceView = shareButton
|
||||
popover.sourceRect = shareButton.bounds
|
||||
}
|
||||
|
||||
present(activityVC, animated: true)
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Share Items
|
||||
|
||||
```swift
|
||||
class ShareItem: NSObject, UIActivityItemSource {
|
||||
let title: String
|
||||
let url: URL
|
||||
|
||||
init(title: String, url: URL) {
|
||||
self.title = title
|
||||
self.url = url
|
||||
}
|
||||
|
||||
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
||||
return url
|
||||
}
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||
return url
|
||||
}
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
|
||||
return title
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Excluding Activities
|
||||
|
||||
```swift
|
||||
let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil)
|
||||
activityVC.excludedActivityTypes = [
|
||||
.addToReadingList,
|
||||
.assignToContact,
|
||||
.print
|
||||
]
|
||||
```
|
||||
|
||||
## App Lifecycle
|
||||
|
||||
```swift
|
||||
class PlayerViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self, selector: #selector(onBackground),
|
||||
name: UIApplication.didEnterBackgroundNotification, object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self, selector: #selector(onForeground),
|
||||
name: UIApplication.willEnterForegroundNotification, object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self, selector: #selector(onTerminate),
|
||||
name: UIApplication.willTerminateNotification, object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func onBackground() {
|
||||
saveState()
|
||||
pausePlayback()
|
||||
}
|
||||
|
||||
@objc private func onForeground() {
|
||||
restoreState()
|
||||
resumePlayback()
|
||||
}
|
||||
|
||||
@objc private func onTerminate() {
|
||||
saveState()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scene Lifecycle (iOS 13+)
|
||||
|
||||
```swift
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
// Resume tasks
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
// Pause tasks
|
||||
}
|
||||
|
||||
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||
// Save state
|
||||
}
|
||||
|
||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||
// Prepare UI
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### State Preservation
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController {
|
||||
override func encodeRestorableState(with coder: NSCoder) {
|
||||
super.encodeRestorableState(with: coder)
|
||||
coder.encode(currentItemID, forKey: "currentItemID")
|
||||
}
|
||||
|
||||
override func decodeRestorableState(with coder: NSCoder) {
|
||||
super.decodeRestorableState(with: coder)
|
||||
if let itemID = coder.decodeObject(forKey: "currentItemID") as? String {
|
||||
loadItem(itemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Haptic Feedback
|
||||
|
||||
```swift
|
||||
func onTaskComplete() {
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
}
|
||||
|
||||
func onError() {
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
}
|
||||
|
||||
func onWarning() {
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.warning)
|
||||
}
|
||||
|
||||
func onSelection() {
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
}
|
||||
|
||||
func onImpact() {
|
||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
}
|
||||
```
|
||||
|
||||
### Impact Styles
|
||||
|
||||
| Style | Usage |
|
||||
|-------|-------|
|
||||
| `.light` | Subtle feedback, small UI changes |
|
||||
| `.medium` | Standard feedback, button presses |
|
||||
| `.heavy` | Strong feedback, significant actions |
|
||||
| `.soft` | Gentle feedback, background changes |
|
||||
| `.rigid` | Sharp feedback, collisions |
|
||||
|
||||
### Prepared Feedback
|
||||
|
||||
For time-critical haptics, prepare the generator in advance:
|
||||
|
||||
```swift
|
||||
class DraggableView: UIView {
|
||||
private let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
impactGenerator.prepare()
|
||||
}
|
||||
|
||||
func didSnapToPosition() {
|
||||
impactGenerator.impactOccurred()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Deep Linking
|
||||
|
||||
### URL Schemes
|
||||
|
||||
```swift
|
||||
// In AppDelegate or SceneDelegate
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch components.host {
|
||||
case "item":
|
||||
if let itemID = components.queryItems?.first(where: { $0.name == "id" })?.value {
|
||||
navigateToItem(itemID)
|
||||
return true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
### Universal Links
|
||||
|
||||
```swift
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
|
||||
let url = userActivity.webpageURL else {
|
||||
return false
|
||||
}
|
||||
|
||||
return handleUniversalLink(url)
|
||||
}
|
||||
```
|
||||
|
||||
## Background Tasks
|
||||
|
||||
```swift
|
||||
import BackgroundTasks
|
||||
|
||||
func registerBackgroundTasks() {
|
||||
BGTaskScheduler.shared.register(
|
||||
forTaskWithIdentifier: "com.app.refresh",
|
||||
using: nil
|
||||
) { task in
|
||||
self.handleAppRefresh(task: task as! BGAppRefreshTask)
|
||||
}
|
||||
}
|
||||
|
||||
func scheduleAppRefresh() {
|
||||
let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
|
||||
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
|
||||
|
||||
do {
|
||||
try BGTaskScheduler.shared.submit(request)
|
||||
} catch {
|
||||
print("Could not schedule app refresh: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func handleAppRefresh(task: BGAppRefreshTask) {
|
||||
scheduleAppRefresh()
|
||||
|
||||
let operation = RefreshOperation()
|
||||
|
||||
task.expirationHandler = {
|
||||
operation.cancel()
|
||||
}
|
||||
|
||||
operation.completionBlock = {
|
||||
task.setTaskCompleted(success: !operation.isCancelled)
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation(operation)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*UIKit, Core Location, and Apple are trademarks of Apple Inc.*
|
||||
297
ios-application-dev/references/uikit-components.md
Normal file
297
ios-application-dev/references/uikit-components.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# UIKit Components
|
||||
|
||||
Common UIKit components guide covering UIStackView, buttons, alerts, search, and context menus.
|
||||
|
||||
## UIStackView
|
||||
|
||||
Stack views simplify auto layout for linear arrangements:
|
||||
|
||||
```swift
|
||||
class FormViewController: UIViewController {
|
||||
private let mainStack = UIStackView()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
mainStack.axis = .vertical
|
||||
mainStack.spacing = 16
|
||||
mainStack.alignment = .fill
|
||||
mainStack.distribution = .fill
|
||||
|
||||
view.addSubview(mainStack)
|
||||
mainStack.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(20)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
}
|
||||
|
||||
let headerStack = UIStackView()
|
||||
headerStack.axis = .horizontal
|
||||
headerStack.spacing = 12
|
||||
headerStack.alignment = .center
|
||||
|
||||
let avatarView = UIImageView()
|
||||
avatarView.snp.makeConstraints { make in
|
||||
make.size.equalTo(48)
|
||||
}
|
||||
|
||||
let labelStack = UIStackView()
|
||||
labelStack.axis = .vertical
|
||||
labelStack.spacing = 4
|
||||
labelStack.addArrangedSubview(titleLabel)
|
||||
labelStack.addArrangedSubview(subtitleLabel)
|
||||
|
||||
headerStack.addArrangedSubview(avatarView)
|
||||
headerStack.addArrangedSubview(labelStack)
|
||||
|
||||
mainStack.addArrangedSubview(headerStack)
|
||||
mainStack.addArrangedSubview(contentView)
|
||||
mainStack.addArrangedSubview(actionButton)
|
||||
|
||||
mainStack.setCustomSpacing(24, after: headerStack)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### StackView Properties
|
||||
|
||||
| Property | Options | Usage |
|
||||
|----------|---------|-------|
|
||||
| `axis` | `.horizontal`, `.vertical` | Layout direction |
|
||||
| `distribution` | `.fill`, `.fillEqually`, `.fillProportionally`, `.equalSpacing`, `.equalCentering` | Space distribution |
|
||||
| `alignment` | `.fill`, `.leading`, `.center`, `.trailing` | Cross-axis alignment |
|
||||
| `spacing` | CGFloat | Uniform spacing |
|
||||
| `setCustomSpacing(_:after:)` | - | Variable spacing |
|
||||
|
||||
## UIButton.Configuration (iOS 15+)
|
||||
|
||||
```swift
|
||||
let primaryButton = UIButton(type: .system)
|
||||
primaryButton.configuration = .filled()
|
||||
primaryButton.setTitle("Continue", for: .normal)
|
||||
|
||||
let secondaryButton = UIButton(type: .system)
|
||||
secondaryButton.configuration = .tinted()
|
||||
secondaryButton.setTitle("Save for Later", for: .normal)
|
||||
|
||||
let destructiveButton = UIButton(type: .system)
|
||||
destructiveButton.configuration = .plain()
|
||||
destructiveButton.setTitle("Remove", for: .normal)
|
||||
destructiveButton.tintColor = .systemRed
|
||||
```
|
||||
|
||||
### Custom Button Configuration
|
||||
|
||||
```swift
|
||||
var config = UIButton.Configuration.filled()
|
||||
config.title = "Add to Cart"
|
||||
config.image = UIImage(systemName: "cart.badge.plus")
|
||||
config.imagePadding = 8
|
||||
config.cornerStyle = .capsule
|
||||
config.baseBackgroundColor = .systemBlue
|
||||
config.baseForegroundColor = .white
|
||||
let cartButton = UIButton(configuration: config)
|
||||
```
|
||||
|
||||
### Button State Handling
|
||||
|
||||
```swift
|
||||
var config = UIButton.Configuration.filled()
|
||||
config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
|
||||
var outgoing = incoming
|
||||
outgoing.font = .boldSystemFont(ofSize: 16)
|
||||
return outgoing
|
||||
}
|
||||
|
||||
config.configurationUpdateHandler = { button in
|
||||
var config = button.configuration
|
||||
config?.showsActivityIndicator = button.isSelected
|
||||
button.configuration = config
|
||||
}
|
||||
```
|
||||
|
||||
## UIAlertController
|
||||
|
||||
### Alert
|
||||
|
||||
```swift
|
||||
func confirmDeletion() {
|
||||
let alert = UIAlertController(
|
||||
title: "Remove Item?",
|
||||
message: "This cannot be undone.",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "Remove", style: .destructive) { _ in
|
||||
self.performDeletion()
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
```
|
||||
|
||||
### Action Sheet
|
||||
|
||||
```swift
|
||||
func showOptions() {
|
||||
let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
sheet.addAction(UIAlertAction(title: "Share", style: .default) { _ in })
|
||||
sheet.addAction(UIAlertAction(title: "Edit", style: .default) { _ in })
|
||||
sheet.addAction(UIAlertAction(title: "Delete", style: .destructive) { _ in })
|
||||
sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||
|
||||
if let popover = sheet.popoverPresentationController {
|
||||
popover.sourceView = optionsButton
|
||||
popover.sourceRect = optionsButton.bounds
|
||||
}
|
||||
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
```
|
||||
|
||||
### Alert with Text Field
|
||||
|
||||
```swift
|
||||
func showInputAlert() {
|
||||
let alert = UIAlertController(
|
||||
title: "Rename",
|
||||
message: "Enter a new name",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
|
||||
alert.addTextField { textField in
|
||||
textField.placeholder = "Name"
|
||||
textField.autocapitalizationType = .words
|
||||
}
|
||||
|
||||
alert.addAction(UIAlertAction(title: "Save", style: .default) { _ in
|
||||
if let name = alert.textFields?.first?.text {
|
||||
self.rename(to: name)
|
||||
}
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||
|
||||
present(alert, animated: true)
|
||||
}
|
||||
```
|
||||
|
||||
## UISearchController
|
||||
|
||||
```swift
|
||||
class SearchableListVC: UIViewController, UISearchResultsUpdating {
|
||||
private let searchController = UISearchController(searchResultsController: nil)
|
||||
private var allItems: [Item] = []
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupSearch()
|
||||
}
|
||||
|
||||
private func setupSearch() {
|
||||
searchController.searchResultsUpdater = self
|
||||
searchController.obscuresBackgroundDuringPresentation = false
|
||||
searchController.searchBar.placeholder = "Search"
|
||||
navigationItem.searchController = searchController
|
||||
definesPresentationContext = true
|
||||
}
|
||||
|
||||
func updateSearchResults(for searchController: UISearchController) {
|
||||
let query = searchController.searchBar.text ?? ""
|
||||
let filtered = query.isEmpty ? allItems : allItems.filter {
|
||||
$0.title.localizedCaseInsensitiveContains(query)
|
||||
}
|
||||
updateItems(filtered)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Search Bar Configuration
|
||||
|
||||
```swift
|
||||
searchController.searchBar.scopeButtonTitles = ["All", "Recent", "Favorites"]
|
||||
searchController.searchBar.showsScopeBar = true
|
||||
searchController.searchBar.delegate = self
|
||||
|
||||
extension SearchableListVC: UISearchBarDelegate {
|
||||
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
|
||||
filterContent(scope: selectedScope)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## UIContextMenuInteraction
|
||||
|
||||
```swift
|
||||
extension PhotoCell: UIContextMenuInteractionDelegate {
|
||||
func contextMenuInteraction(
|
||||
_ interaction: UIContextMenuInteraction,
|
||||
configurationForMenuAtLocation location: CGPoint
|
||||
) -> UIContextMenuConfiguration? {
|
||||
UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
|
||||
let share = UIAction(
|
||||
title: "Share",
|
||||
image: UIImage(systemName: "square.and.arrow.up")
|
||||
) { _ in }
|
||||
|
||||
let favorite = UIAction(
|
||||
title: "Favorite",
|
||||
image: UIImage(systemName: "heart")
|
||||
) { _ in }
|
||||
|
||||
let delete = UIAction(
|
||||
title: "Delete",
|
||||
image: UIImage(systemName: "trash"),
|
||||
attributes: .destructive
|
||||
) { _ in }
|
||||
|
||||
return UIMenu(children: [share, favorite, delete])
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Context Menu with Preview
|
||||
|
||||
```swift
|
||||
func contextMenuInteraction(
|
||||
_ interaction: UIContextMenuInteraction,
|
||||
configurationForMenuAtLocation location: CGPoint
|
||||
) -> UIContextMenuConfiguration? {
|
||||
UIContextMenuConfiguration(
|
||||
identifier: itemID as NSCopying,
|
||||
previewProvider: { [weak self] in
|
||||
return self?.makePreviewController()
|
||||
},
|
||||
actionProvider: { _ in
|
||||
return self.makeMenu()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func contextMenuInteraction(
|
||||
_ interaction: UIContextMenuInteraction,
|
||||
willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration,
|
||||
animator: UIContextMenuInteractionCommitAnimating
|
||||
) {
|
||||
animator.addCompletion {
|
||||
self.showDetail()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CollectionView Context Menu
|
||||
|
||||
```swift
|
||||
func collectionView(
|
||||
_ collectionView: UICollectionView,
|
||||
contextMenuConfigurationForItemAt indexPath: IndexPath,
|
||||
point: CGPoint
|
||||
) -> UIContextMenuConfiguration? {
|
||||
let item = dataSource.itemIdentifier(for: indexPath)
|
||||
return UIContextMenuConfiguration(identifier: indexPath as NSCopying, previewProvider: nil) { _ in
|
||||
return self.makeMenu(for: item)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*UIKit and Apple are trademarks of Apple Inc.*
|
||||
Reference in New Issue
Block a user