Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
178
ios-application-dev/SKILL.md
Normal file
178
ios-application-dev/SKILL.md
Normal file
@@ -0,0 +1,178 @@
|
||||
---
|
||||
name: ios-application-dev
|
||||
description: |
|
||||
iOS application development guide covering UIKit, SnapKit, and SwiftUI. Includes touch targets, safe areas, navigation patterns, Dynamic Type, Dark Mode, accessibility, collection views, common UI components, and SwiftUI design guidelines. For detailed references on specific topics, see the reference files.
|
||||
Use when: developing iOS apps, implementing UI, reviewing iOS code, working with UIKit/SnapKit/SwiftUI layouts, building iPhone interfaces, Swift mobile development, Apple HIG compliance, iOS accessibility implementation.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: MiniMax-OpenSource
|
||||
version: "1.0.0"
|
||||
category: mobile
|
||||
sources:
|
||||
- Apple Human Interface Guidelines
|
||||
- Apple Developer Documentation
|
||||
---
|
||||
|
||||
# iOS Application Development Guide
|
||||
|
||||
A practical guide for building iOS applications using UIKit, SnapKit, and SwiftUI. Focuses on proven patterns and Apple platform conventions.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### UIKit
|
||||
|
||||
| Purpose | Component |
|
||||
|---------|-----------|
|
||||
| Main sections | `UITabBarController` |
|
||||
| Drill-down | `UINavigationController` |
|
||||
| Focused task | Sheet presentation |
|
||||
| Critical choice | `UIAlertController` |
|
||||
| Secondary actions | `UIContextMenuInteraction` |
|
||||
| List content | `UICollectionView` + `DiffableDataSource` |
|
||||
| Sectioned list | `DiffableDataSource` + `headerMode` |
|
||||
| Grid layout | `UICollectionViewCompositionalLayout` |
|
||||
| Search | `UISearchController` |
|
||||
| Share | `UIActivityViewController` |
|
||||
| Location (once) | `CLLocationButton` |
|
||||
| Feedback | `UIImpactFeedbackGenerator` |
|
||||
| Linear layout | `UIStackView` |
|
||||
| Custom shapes | `CAShapeLayer` + `UIBezierPath` |
|
||||
| Gradients | `CAGradientLayer` |
|
||||
| Modern buttons | `UIButton.Configuration` |
|
||||
| Dynamic text | `UIFontMetrics` + `preferredFont` |
|
||||
| Dark mode | Semantic colors (`.systemBackground`, `.label`) |
|
||||
| Permissions | Contextual request + `AVCaptureDevice` |
|
||||
| Lifecycle | `UIApplication` notifications |
|
||||
|
||||
### SwiftUI
|
||||
|
||||
| Purpose | Component |
|
||||
|---------|-----------|
|
||||
| Main sections | `TabView` + `tabItem` |
|
||||
| Drill-down | `NavigationStack` + `NavigationPath` |
|
||||
| Focused task | `.sheet` + `presentationDetents` |
|
||||
| Critical choice | `.alert` |
|
||||
| Secondary actions | `.contextMenu` |
|
||||
| List content | `List` + `.insetGrouped` |
|
||||
| Search | `.searchable` |
|
||||
| Share | `ShareLink` |
|
||||
| Location (once) | `LocationButton` |
|
||||
| Feedback | `UIImpactFeedbackGenerator` |
|
||||
| Progress (known) | `ProgressView(value:total:)` |
|
||||
| Progress (unknown) | `ProgressView()` |
|
||||
| Dynamic text | `.font(.body)` semantic styles |
|
||||
| Dark mode | `.primary`, `.secondary`, `Color(.systemBackground)` |
|
||||
| Scene lifecycle | `@Environment(\.scenePhase)` |
|
||||
| Reduce motion | `@Environment(\.accessibilityReduceMotion)` |
|
||||
| Dynamic type | `@Environment(\.dynamicTypeSize)` |
|
||||
|
||||
## Core Principles
|
||||
|
||||
### Layout
|
||||
- Touch targets >= 44pt
|
||||
- Content within safe areas (SwiftUI respects by default, use `.ignoresSafeArea()` only for backgrounds)
|
||||
- Use 8pt spacing increments (8, 16, 24, 32, 40, 48)
|
||||
- Primary actions in thumb zone
|
||||
- Support all screen sizes (iPhone SE 375pt to Pro Max 430pt)
|
||||
|
||||
### Typography
|
||||
- UIKit: `preferredFont(forTextStyle:)` + `adjustsFontForContentSizeCategory = true`
|
||||
- SwiftUI: semantic text styles `.headline`, `.body`, `.caption`
|
||||
- Custom fonts: `UIFontMetrics` / `Font.custom(_:size:relativeTo:)`
|
||||
- Adapt layout at accessibility sizes (minimum 11pt)
|
||||
|
||||
### Colors
|
||||
- Use semantic system colors (`.systemBackground`, `.label`, `.primary`, `.secondary`)
|
||||
- Asset catalog variants for custom colors (Any/Dark Appearance)
|
||||
- No color-only information (pair with icons or text)
|
||||
- Contrast ratio >= 4.5:1 for normal text, 3:1 for large text
|
||||
|
||||
### Accessibility
|
||||
- Labels on icon buttons (`.accessibilityLabel()`)
|
||||
- Reduce motion respected (`@Environment(\.accessibilityReduceMotion)`)
|
||||
- Logical reading order (`.accessibilitySortPriority()`)
|
||||
- Support Bold Text, Increase Contrast preferences
|
||||
|
||||
### Navigation
|
||||
- Tab bar (3-5 sections) stays visible during navigation
|
||||
- Back swipe works (never override system gestures)
|
||||
- State preserved across tabs (`@SceneStorage`, `@State`)
|
||||
- Never use hamburger menus
|
||||
|
||||
### Privacy & Permissions
|
||||
- Request permissions in context (not at launch)
|
||||
- Custom explanation before system dialog
|
||||
- Support Sign in with Apple
|
||||
- Respect ATT denial
|
||||
|
||||
## Checklist
|
||||
|
||||
### Layout
|
||||
- [ ] Touch targets >= 44pt
|
||||
- [ ] Content within safe areas
|
||||
- [ ] Primary actions in thumb zone (bottom half)
|
||||
- [ ] Flexible widths for all screen sizes (SE to Pro Max)
|
||||
- [ ] Spacing aligns to 8pt grid
|
||||
|
||||
### Typography
|
||||
- [ ] Semantic text styles or UIFontMetrics-scaled custom fonts
|
||||
- [ ] Dynamic Type supported up to accessibility sizes
|
||||
- [ ] Layouts reflow at large sizes (no truncation)
|
||||
- [ ] Minimum text size 11pt
|
||||
|
||||
### Colors
|
||||
- [ ] Semantic system colors or light/dark asset variants
|
||||
- [ ] Dark Mode is intentional (not just inverted)
|
||||
- [ ] No color-only information
|
||||
- [ ] Text contrast >= 4.5:1 (normal) / 3:1 (large)
|
||||
- [ ] Single accent color for interactive elements
|
||||
|
||||
### Accessibility
|
||||
- [ ] VoiceOver labels on all interactive elements
|
||||
- [ ] Logical reading order
|
||||
- [ ] Bold Text preference respected
|
||||
- [ ] Reduce Motion disables decorative animations
|
||||
- [ ] All gestures have alternative access paths
|
||||
|
||||
### Navigation
|
||||
- [ ] Tab bar for 3-5 top-level sections
|
||||
- [ ] No hamburger/drawer menus
|
||||
- [ ] Tab bar stays visible during navigation
|
||||
- [ ] Back swipe works throughout
|
||||
- [ ] State preserved across tabs
|
||||
|
||||
### Components
|
||||
- [ ] Alerts for critical decisions only
|
||||
- [ ] Sheets have dismiss path (button and/or swipe)
|
||||
- [ ] List rows >= 44pt tall
|
||||
- [ ] Destructive buttons use `.destructive` role
|
||||
|
||||
### Privacy
|
||||
- [ ] Permissions requested in context (not at launch)
|
||||
- [ ] Custom explanation before system permission dialog
|
||||
- [ ] Sign in with Apple offered with other providers
|
||||
- [ ] Basic features usable without account
|
||||
- [ ] ATT prompt shown if tracking, denial respected
|
||||
|
||||
### System Integration
|
||||
- [ ] App handles interruptions gracefully (calls, background, Siri)
|
||||
- [ ] App content indexed for Spotlight
|
||||
- [ ] Share Sheet available for shareable content
|
||||
|
||||
## References
|
||||
|
||||
| Topic | Reference |
|
||||
|-------|-----------|
|
||||
| Touch Targets, Safe Area, CollectionView | [Layout System](references/layout-system.md) |
|
||||
| TabBar, NavigationController, Modal | [Navigation Patterns](references/navigation-patterns.md) |
|
||||
| StackView, Button, Alert, Search, ContextMenu | [UIKit Components](references/uikit-components.md) |
|
||||
| CAShapeLayer, CAGradientLayer, Core Animation | [Graphics & Animation](references/graphics-animation.md) |
|
||||
| Dynamic Type, Semantic Colors, VoiceOver | [Accessibility](references/accessibility.md) |
|
||||
| Permissions, Location, Share, Lifecycle, Haptics | [System Integration](references/system-integration.md) |
|
||||
| Metal Shaders & GPU | [Metal Shader Reference](references/metal-shader.md) |
|
||||
| SwiftUI HIG, Components, Patterns, Anti-Patterns | [SwiftUI Design Guidelines](references/swiftui-design-guidelines.md) |
|
||||
| Optionals, Protocols, async/await, ARC, Error Handling | [Swift Coding Standards](references/swift-coding-standards.md) |
|
||||
|
||||
---
|
||||
|
||||
Swift, SwiftUI, UIKit, SF Symbols, Metal, and Apple are trademarks of Apple Inc. SnapKit is a trademark of its respective owners.
|
||||
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