<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>SwiftUI » MszPro・株式会社Smartソフト</title>
	<atom:link href="https://mszpro.com/category/swiftui/feed" rel="self" type="application/rss+xml" />
	<link>https://mszpro.com</link>
	<description>iOS VisionOS SwiftUI Programming Blog. Dream it, Chase it, Code it.</description>
	<lastBuildDate>Wed, 18 Dec 2024 07:10:26 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.8.1</generator>

<image>
	<url>https://static-assets.mszpro.com/2024/12/cropped-Unknown-32x32.webp</url>
	<title>SwiftUI » MszPro・株式会社Smartソフト</title>
	<link>https://mszpro.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Adapt to physical camera control button within your own iOS app (for SwiftUI, Zoom, Exposure, &#038; Custom Controls)</title>
		<link>https://mszpro.com/ios-camera-control-button</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Wed, 18 Dec 2024 07:04:47 +0000</pubDate>
				<category><![CDATA[iOS]]></category>
		<category><![CDATA[iOS 18]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=541</guid>

					<description><![CDATA[<p>Use the physical camera button within your own iOS app to allow user to take pictures, use zoom and exposure control, and add your own control options (WWDC 2024, AVCaptureSessionControlsDelegate, AVCaptureSystemZoomSlider, AVCaptureSystemExposureBiasSlider, AVCaptureSlider, AVCaptureIndexPicker)</p>
<p>The post <a href="https://mszpro.com/ios-camera-control-button">Adapt to physical camera control button within your own iOS app (for SwiftUI, Zoom, Exposure, & Custom Controls)</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>This article talks about adapting the new physical camera button within your iOS app.</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="502" src="https://static-assets.mszpro.com/2024/12/time-travel-with-mszpro-1024x502.png" alt="" class="wp-image-553" srcset="https://static-assets.mszpro.com/2024/12/time-travel-with-mszpro-300x147.png 300w, https://static-assets.mszpro.com/2024/12/time-travel-with-mszpro-1024x502.png 1024w, https://static-assets.mszpro.com/2024/12/time-travel-with-mszpro-768x376.png 768w, https://static-assets.mszpro.com/2024/12/time-travel-with-mszpro-1536x753.png 1536w, https://static-assets.mszpro.com/2024/12/time-travel-with-mszpro-2048x1004.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="502" src="https://static-assets.mszpro.com/2024/12/qiita-switch-1-1024x502.png" alt="" class="wp-image-554" srcset="https://static-assets.mszpro.com/2024/12/qiita-switch-1-300x147.png 300w, https://static-assets.mszpro.com/2024/12/qiita-switch-1-1024x502.png 1024w, https://static-assets.mszpro.com/2024/12/qiita-switch-1-768x376.png 768w, https://static-assets.mszpro.com/2024/12/qiita-switch-1-1536x753.png 1536w, https://static-assets.mszpro.com/2024/12/qiita-switch-1-2048x1004.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h1 class="wp-block-heading">Why?</h1>



<p>If you do nothing, and when the user presses the camera button within your app, it always goes to the system Camera app. However, with some minor adjustments, user can use the camera button to take pictures within your own app, and your app can provide custom controls to the camera, for example, allow the user to change the filter by swiping on the physical camera button.</p>



<p>Also, you can add cool new control options, like shown in the above 2 screenshots.</p>



<p>Let&#8217;s get started!</p>



<h1 class="wp-block-heading">Starting point</h1>



<p>We will start from a very simple SwiftUI view that shows a camera and a capture button:</p>



<p>This is the view model that manages the permission to access camera, and take actions when we need to take a picture, and receives the image and saves it to a variable.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="// MARK: - Unified Camera ViewModel
class CameraViewModel: NSObject, ObservableObject {
    
    // Session states
    enum CameraSetupState {
        case idle
        case configured
        case permissionDenied
        case failed
    }
    
    @Published var setupState: CameraSetupState = .idle
    @Published var capturedPhoto: UIImage? = nil
    @Published var permissionGranted: Bool = false
    
    let session = AVCaptureSession()
    private let photoOutput = AVCapturePhotoOutput()
    private var videoInput: AVCaptureDeviceInput?
    
    // Dispatch queue for configuring the session
    private let configurationQueue = DispatchQueue(label: &quot;com.example.camera.config&quot;)
    
    override init() {
        super.init()
    }
    
    deinit {
        stopSession()
    }
    
    // MARK: - Public API
    
    /// Checks camera permissions and configures session if authorized.
    func requestAccessIfNeeded() {
        let authStatus = AVCaptureDevice.authorizationStatus(for: .video)
        switch authStatus {
            case .authorized:
                permissionGranted = true
                configureSessionIfIdle()
            case .notDetermined:
                AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
                    guard let self = self else { return }
                    DispatchQueue.main.async {
                        if granted {
                            self.permissionGranted = true
                            self.configureSessionIfIdle()
                        } else {
                            self.setupState = .permissionDenied
                        }
                    }
                }
            default:
                // Denied or Restricted
                setupState = .permissionDenied
        }
    }
    
    /// Initiate photo capture.
    func capturePhoto() {
        guard setupState == .configured else { return }
        let settings = AVCapturePhotoSettings()
        photoOutput.capturePhoto(with: settings, delegate: self)
    }
    
    // MARK: - Session Configuration
    
    private func configureSessionIfIdle() {
        configurationQueue.async { [weak self] in
            guard let self = self, self.setupState == .idle else { return }
            
            self.session.beginConfiguration()
            self.session.sessionPreset = .photo
            
            self.addCameraInput()
            self.addPhotoOutput()
            
            self.session.commitConfiguration()
            self.startSessionIfReady()
        }
    }
    
    private func addCameraInput() {
        do {
            guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera,
                                                           for: .video,
                                                           position: .back) else {
                print(&quot;CameraViewModel: Back camera is unavailable.&quot;)
                setupState = .idle
                session.commitConfiguration()
                return
            }
            
            let cameraInput = try AVCaptureDeviceInput(device: backCamera)
            if session.canAddInput(cameraInput) {
                session.addInput(cameraInput)
                videoInput = cameraInput
                DispatchQueue.main.async {
                    self.setupState = .configured
                }
            } else {
                print(&quot;CameraViewModel: Unable to add camera input to session.&quot;)
                setupState = .idle
                session.commitConfiguration()
            }
        } catch {
            print(&quot;CameraViewModel: Error creating camera input - \(error)&quot;)
            setupState = .failed
            session.commitConfiguration()
        }
    }
    
    private func addPhotoOutput() {
        guard session.canAddOutput(photoOutput) else {
            print(&quot;CameraViewModel: Cannot add photo output.&quot;)
            setupState = .failed
            session.commitConfiguration()
            return
        }
        session.addOutput(photoOutput)
        photoOutput.maxPhotoQualityPrioritization = .quality
        DispatchQueue.main.async {
            self.setupState = .configured
        }
    }
    
    private func startSessionIfReady() {
        guard setupState == .configured else { return }
        session.startRunning()
    }
    
    private func stopSession() {
        configurationQueue.async { [weak self] in
            guard let self = self else { return }
            if self.session.isRunning {
                self.session.stopRunning()
            }
        }
    }
}

// MARK: - AVCapturePhotoCaptureDelegate
extension CameraViewModel: AVCapturePhotoCaptureDelegate {
    func photoOutput(_ output: AVCapturePhotoOutput,
                     didFinishProcessingPhoto photo: AVCapturePhoto,
                     error: Error?) {
        
        guard error == nil else {
            print(&quot;CameraViewModel: Error capturing photo - \(error!)&quot;)
            return
        }
        guard let photoData = photo.fileDataRepresentation() else {
            print(&quot;CameraViewModel: No photo data found.&quot;)
            return
        }
        self.capturedPhoto = UIImage(data: photoData)
    }
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">// MARK: - Unified Camera ViewModel</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">CameraViewModel</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">NSObject</span><span style="color: #D8DEE9FF">, </span><span style="color: #8FBCBB; font-weight: bold">ObservableObject </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// Session states</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">enum</span><span style="color: #D8DEE9FF"> CameraSetupState </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">idle</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">configured</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">permissionDenied</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">failed</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #81A1C1">Published</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> setupState: CameraSetupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">idle</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #81A1C1">Published</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> capturedPhoto: UIImage</span><span style="color: #81A1C1">?</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">nil</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #81A1C1">Published</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> permissionGranted: </span><span style="color: #8FBCBB">Bool</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">false</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> session </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureSession</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> photoOutput </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCapturePhotoOutput</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> videoInput: AVCaptureDeviceInput</span><span style="color: #81A1C1">?</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// Dispatch queue for configuring the session</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> configurationQueue </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">DispatchQueue</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">label</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">com.example.camera.config</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">override</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">init</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">super</span><span style="color: #D8DEE9FF">.</span><span style="color: #81A1C1">init</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">deinit</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">stopSession</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// MARK: - Public API</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">/// Checks camera permissions and configures session if authorized.</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">requestAccessIfNeeded</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> authStatus </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> AVCaptureDevice.</span><span style="color: #88C0D0">authorizationStatus</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">for</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">video</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">switch</span><span style="color: #D8DEE9FF"> authStatus </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">authorized</span><span style="color: #81A1C1">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">                permissionGranted </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">configureSessionIfIdle</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">notDetermined</span><span style="color: #81A1C1">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">                AVCaptureDevice.</span><span style="color: #88C0D0">requestAccess</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">for</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">video</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> [</span><span style="color: #81A1C1">weak</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">] granted </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    DispatchQueue.</span><span style="color: #D8DEE9">main</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> granted </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">permissionGranted</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">configureSessionIfIdle</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">setupState</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">permissionDenied</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">default:</span></span>
<span class="line"><span style="color: #ECEFF4">                </span><span style="color: #616E88">// Denied or Restricted</span></span>
<span class="line"><span style="color: #D8DEE9FF">                setupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">permissionDenied</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">/// Initiate photo capture.</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">capturePhoto</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> setupState </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> .configured </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> settings </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCapturePhotoSettings</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">        photoOutput.</span><span style="color: #88C0D0">capturePhoto</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">with</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> settings, </span><span style="color: #88C0D0">delegate</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// MARK: - Session Configuration</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">configureSessionIfIdle</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        configurationQueue.</span><span style="color: #88C0D0">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> [</span><span style="color: #81A1C1">weak</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">] </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">, </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.setupState </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> .idle </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">beginConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">sessionPreset</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">photo</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">addCameraInput</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">addPhotoOutput</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">commitConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">startSessionIfReady</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">addCameraInput</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">do</span><span style="color: #ECEFF4"> {</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> backCamera </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> AVCaptureDevice.</span><span style="color: #88C0D0">default</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">builtInWideAngleCamera</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                           </span><span style="color: #88C0D0">for</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">video</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                           </span><span style="color: #88C0D0">position</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">back</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: Back camera is unavailable.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                setupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">idle</span></span>
<span class="line"><span style="color: #D8DEE9FF">                session.</span><span style="color: #88C0D0">commitConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> cameraInput </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">try</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureDeviceInput</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">device</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> backCamera</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> session.</span><span style="color: #88C0D0">canAddInput</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">cameraInput</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                session.</span><span style="color: #88C0D0">addInput</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">cameraInput</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                videoInput </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> cameraInput</span></span>
<span class="line"><span style="color: #D8DEE9FF">                DispatchQueue.</span><span style="color: #D8DEE9">main</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">setupState</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">configured</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: Unable to add camera input to session.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                setupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">idle</span></span>
<span class="line"><span style="color: #D8DEE9FF">                session.</span><span style="color: #88C0D0">commitConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">catch</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: Error creating camera input - </span><span style="color: #81A1C1">\(</span><span style="color: #A3BE8C">error</span><span style="color: #81A1C1">)</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            setupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">failed</span></span>
<span class="line"><span style="color: #D8DEE9FF">            session.</span><span style="color: #88C0D0">commitConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">addPhotoOutput</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> session.</span><span style="color: #88C0D0">canAddOutput</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">photoOutput</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: Cannot add photo output.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            setupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">failed</span></span>
<span class="line"><span style="color: #D8DEE9FF">            session.</span><span style="color: #88C0D0">commitConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        session.</span><span style="color: #88C0D0">addOutput</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">photoOutput</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">        photoOutput.</span><span style="color: #D8DEE9">maxPhotoQualityPrioritization</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">quality</span></span>
<span class="line"><span style="color: #D8DEE9FF">        DispatchQueue.</span><span style="color: #D8DEE9">main</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">setupState</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">configured</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">startSessionIfReady</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> setupState </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> .configured </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        session.</span><span style="color: #88C0D0">startRunning</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">stopSession</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        configurationQueue.</span><span style="color: #88C0D0">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> [</span><span style="color: #81A1C1">weak</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">] </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.session.isRunning </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">stopRunning</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// MARK: - AVCapturePhotoCaptureDelegate</span></span>
<span class="line"><span style="color: #81A1C1">extension</span><span style="color: #D8DEE9FF"> CameraViewModel</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">AVCapturePhotoCaptureDelegate </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">photoOutput</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">output</span><span style="color: #D8DEE9FF">: AVCapturePhotoOutput,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                     </span><span style="color: #88C0D0">didFinishProcessingPhoto</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">photo</span><span style="color: #D8DEE9FF">: AVCapturePhoto,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                     </span><span style="color: #88C0D0">error</span><span style="color: #D8DEE9FF">: </span><span style="color: #8FBCBB">Error</span><span style="color: #81A1C1">?</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> error </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">nil</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: Error capturing photo - </span><span style="color: #81A1C1">\(</span><span style="color: #A3BE8C">error</span><span style="color: #81A1C1">!)</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> photoData </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> photo.</span><span style="color: #88C0D0">fileDataRepresentation</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: No photo data found.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">capturedPhoto</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">UIImage</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">data</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> photoData</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>This is our UIKit compatible view so we can show the camera preview layer within SwiftUI:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="// MARK: - SwiftUI Representable for Camera Preview
struct CameraLayerView: UIViewRepresentable {
    
    let cameraSession: AVCaptureSession
    
    func makeUIView(context: Context) -&gt; CameraContainerView {
        let container = CameraContainerView()
        container.backgroundColor = .black
        container.previewLayer.session = cameraSession
        container.previewLayer.videoGravity = .resizeAspect
        return container
    }
    
    func updateUIView(_ uiView: CameraContainerView, context: Context) {
        // No dynamic updates needed
    }
    
    // A UIView subclass that hosts an AVCaptureVideoPreviewLayer
    class CameraContainerView: UIView {
        
        override class var layerClass: AnyClass {
            AVCaptureVideoPreviewLayer.self
        }
        
        var previewLayer: AVCaptureVideoPreviewLayer {
            guard let layer = self.layer as? AVCaptureVideoPreviewLayer else {
                fatalError(&quot;CameraContainerView: Failed casting layer to AVCaptureVideoPreviewLayer.&quot;)
            }
            return layer
        }
    }
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">// MARK: - SwiftUI Representable for Camera Preview</span></span>
<span class="line"><span style="color: #81A1C1">struct</span><span style="color: #D8DEE9FF"> CameraLayerView</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">UIViewRepresentable </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> cameraSession: AVCaptureSession</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">makeUIView</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">context</span><span style="color: #D8DEE9FF">: Context</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">-&gt;</span><span style="color: #D8DEE9FF"> CameraContainerView </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> container </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">CameraContainerView</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">        container.</span><span style="color: #D8DEE9">backgroundColor</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">black</span></span>
<span class="line"><span style="color: #D8DEE9FF">        container.</span><span style="color: #D8DEE9">previewLayer</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> cameraSession</span></span>
<span class="line"><span style="color: #D8DEE9FF">        container.</span><span style="color: #D8DEE9">previewLayer</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">videoGravity</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">resizeAspect</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> container</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">updateUIView</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">uiView</span><span style="color: #D8DEE9FF">: CameraContainerView, </span><span style="color: #88C0D0">context</span><span style="color: #D8DEE9FF">: Context</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #ECEFF4">        </span><span style="color: #616E88">// No dynamic updates needed</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// A UIView subclass that hosts an AVCaptureVideoPreviewLayer</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">CameraContainerView</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">UIView </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">override</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> layerClass: </span><span style="color: #8FBCBB">AnyClass</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            AVCaptureVideoPreviewLayer.</span><span style="color: #81A1C1">self</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> previewLayer: AVCaptureVideoPreviewLayer </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> layer </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.layer </span><span style="color: #81A1C1">as?</span><span style="color: #D8DEE9FF"> AVCaptureVideoPreviewLayer </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">fatalError</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraContainerView: Failed casting layer to AVCaptureVideoPreviewLayer.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> layer</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>And this is our main SwiftUI view:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="// MARK: - SwiftUI Main View
struct ContentView: View {
    
    @ObservedObject var viewModel = CameraViewModel()
    
    var body: some View {
        GeometryReader { _ in
            ZStack(alignment: .bottom) {
                CameraLayerView(cameraSession: viewModel.session)
                    .onAppear {
                        viewModel.requestAccessIfNeeded()
                    }
                    .edgesIgnoringSafeArea(.all)
                
                // Capture button
                VStack {
                    Spacer()
                    
                    Button {
                        viewModel.capturePhoto()
                    } label: {
                        Text(&quot;Take Photo&quot;)
                            .font(.headline)
                            .foregroundColor(.white)
                            .padding()
                            .background(Color.blue)
                            .cornerRadius(10)
                    }
                    .padding(.bottom, 20)
                }
                
                // Thumbnail of the captured photo
                if let image = viewModel.capturedPhoto {
                    Image(uiImage: image)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 120, height: 90)
                        .padding(.bottom, 80)
                }
            }
        }
    }
}

// MARK: - SwiftUI Preview
#Preview {
    ContentView()
}
" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">// MARK: - SwiftUI Main View</span></span>
<span class="line"><span style="color: #81A1C1">struct</span><span style="color: #D8DEE9FF"> ContentView</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">View </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #81A1C1">ObservedObject</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> viewModel </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">CameraViewModel</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> body: </span><span style="color: #81A1C1">some</span><span style="color: #D8DEE9FF"> View </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">GeometryReader</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> _ </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #88C0D0">ZStack</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">alignment</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">bottom</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">CameraLayerView</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">cameraSession</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> viewModel.</span><span style="color: #D8DEE9">session</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    .</span><span style="color: #88C0D0">onAppear</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        viewModel.</span><span style="color: #88C0D0">requestAccessIfNeeded</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    .</span><span style="color: #88C0D0">edgesIgnoringSafeArea</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">all</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span></span>
<span class="line"><span style="color: #ECEFF4">                </span><span style="color: #616E88">// Capture button</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">VStack</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #88C0D0">Spacer</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #88C0D0">Button</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        viewModel.</span><span style="color: #88C0D0">capturePhoto</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">label</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #88C0D0">Text</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Take Photo</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            .</span><span style="color: #88C0D0">font</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">headline</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            .</span><span style="color: #88C0D0">foregroundColor</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">white</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            .</span><span style="color: #88C0D0">padding</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            .</span><span style="color: #88C0D0">background</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">Color.</span><span style="color: #D8DEE9">blue</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            .</span><span style="color: #88C0D0">cornerRadius</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">10</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    .</span><span style="color: #88C0D0">padding</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">bottom</span><span style="color: #D8DEE9FF">, </span><span style="color: #B48EAD">20</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span></span>
<span class="line"><span style="color: #ECEFF4">                </span><span style="color: #616E88">// Thumbnail of the captured photo</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> image </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> viewModel.capturedPhoto </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #88C0D0">Image</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">uiImage</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> image</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        .</span><span style="color: #88C0D0">resizable</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        .</span><span style="color: #88C0D0">aspectRatio</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">contentMode</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">fit</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        .</span><span style="color: #88C0D0">frame</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">width</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">120</span><span style="color: #D8DEE9FF">, </span><span style="color: #88C0D0">height</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">90</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        .</span><span style="color: #88C0D0">padding</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">bottom</span><span style="color: #D8DEE9FF">, </span><span style="color: #B48EAD">80</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// MARK: - SwiftUI Preview</span></span>
<span class="line"><span style="color: #88C0D0">#Preview</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">ContentView</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span></code></pre></div>



<h1 class="wp-block-heading">Capture picture when pressing camera button</h1>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="575" src="https://static-assets.mszpro.com/2024/12/60989-125717-60951-125647-888-Camera-Control-xl-xl-1024x575.jpg" alt="" class="wp-image-542" srcset="https://static-assets.mszpro.com/2024/12/60989-125717-60951-125647-888-Camera-Control-xl-xl-300x169.jpg 300w, https://static-assets.mszpro.com/2024/12/60989-125717-60951-125647-888-Camera-Control-xl-xl-1024x575.jpg 1024w, https://static-assets.mszpro.com/2024/12/60989-125717-60951-125647-888-Camera-Control-xl-xl-768x431.jpg 768w, https://static-assets.mszpro.com/2024/12/60989-125717-60951-125647-888-Camera-Control-xl-xl.jpg 1280w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>As we talked about, if your app does not adapt this, when the user presses on the camera button, it will jump to the camera app.</p>



<p>To allow your app to handle the take picture action, simply import the <code>AVKit</code> framework and add a view modifier <code>onCameraCaptureEvent()</code> view modifier to your camera preview layer <code>CameraLayerView</code>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="CameraLayerView(cameraSession: viewModel.session)
    .onAppear {
        viewModel.requestAccessIfNeeded()
    }
    .edgesIgnoringSafeArea(.all)
    .onCameraCaptureEvent() { event in
        if event.phase == .began {
            self.viewModel.capturePhoto()
        }
    }" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #88C0D0">CameraLayerView</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">cameraSession</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> viewModel.</span><span style="color: #D8DEE9">session</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    .</span><span style="color: #88C0D0">onAppear</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        viewModel.</span><span style="color: #88C0D0">requestAccessIfNeeded</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    .</span><span style="color: #88C0D0">edgesIgnoringSafeArea</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">all</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    .</span><span style="color: #88C0D0">onCameraCaptureEvent</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> event </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> event.phase </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> .began </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">viewModel</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">capturePhoto</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Now, whenever the user presses the camera button on the side, it will call the code block you provided, which you can call the capture photo function within your view model.</p>



<h1 class="wp-block-heading">Checking support</h1>



<p>You can check if the user&#8217;s device has support for camera button by checking the <code>supportsControls</code> parameter within your camera session <code>AVCaptureSession</code>:</p>



<p>In our provided starting point code, you can call it like this</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="struct ContentView: View {
    
    @ObservedObject var viewModel = CameraViewModel()
    
    var body: some View {
        GeometryReader { _ in
            // ... //
        }
        .task {
            let supportsCameraButton = self.viewModel.session.supportsControls
            
        }
    }
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">struct</span><span style="color: #D8DEE9FF"> ContentView</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">View </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #81A1C1">ObservedObject</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> viewModel </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">CameraViewModel</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> body: </span><span style="color: #81A1C1">some</span><span style="color: #D8DEE9FF"> View </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">GeometryReader</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> _ </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">// ... //</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        .</span><span style="color: #88C0D0">task</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> supportsCameraButton </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">viewModel</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">supportsControls</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<h1 class="wp-block-heading">Add zoom control</h1>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="576" src="https://static-assets.mszpro.com/2024/12/iphone-16-capture-button-1024x576.jpg" alt="" class="wp-image-544" srcset="https://static-assets.mszpro.com/2024/12/iphone-16-capture-button-300x169.jpg 300w, https://static-assets.mszpro.com/2024/12/iphone-16-capture-button-1024x576.jpg 1024w, https://static-assets.mszpro.com/2024/12/iphone-16-capture-button-768x432.jpg 768w, https://static-assets.mszpro.com/2024/12/iphone-16-capture-button-1536x864.jpg 1536w, https://static-assets.mszpro.com/2024/12/iphone-16-capture-button.jpg 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>We can easily add a control for the zoom level. What&#8217;s great is that iOS system handles the zoom automatically, and your app get notified the zoom level to be shown in your own UI:</p>



<div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="471" height="1024" src="https://static-assets.mszpro.com/2024/12/IMG_3276-471x1024.png" alt="" class="wp-image-545" srcset="https://static-assets.mszpro.com/2024/12/IMG_3276-138x300.png 138w, https://static-assets.mszpro.com/2024/12/IMG_3276-471x1024.png 471w, https://static-assets.mszpro.com/2024/12/IMG_3276-768x1669.png 768w, https://static-assets.mszpro.com/2024/12/IMG_3276-707x1536.png 707w, https://static-assets.mszpro.com/2024/12/IMG_3276-943x2048.png 943w, https://static-assets.mszpro.com/2024/12/IMG_3276.png 1320w" sizes="auto, (max-width: 471px) 100vw, 471px" /></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="471" height="1024" src="https://static-assets.mszpro.com/2024/12/IMG_3277-471x1024.png" alt="" class="wp-image-546" srcset="https://static-assets.mszpro.com/2024/12/IMG_3277-138x300.png 138w, https://static-assets.mszpro.com/2024/12/IMG_3277-471x1024.png 471w, https://static-assets.mszpro.com/2024/12/IMG_3277-768x1669.png 768w, https://static-assets.mszpro.com/2024/12/IMG_3277-707x1536.png 707w, https://static-assets.mszpro.com/2024/12/IMG_3277-943x2048.png 943w, https://static-assets.mszpro.com/2024/12/IMG_3277.png 1320w" sizes="auto, (max-width: 471px) 100vw, 471px" /></figure>
</div>
</div>



<p>To get started, first, set a camera control delegate to your camera session:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="self.session.setControlsDelegate(self, queue: self.cameraControlQueue)" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">setControlsDelegate</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">, </span><span style="color: #88C0D0">queue</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">cameraControlQueue</span><span style="color: #ECEFF4">)</span></span></code></pre></div>



<p>Now, conform your class to <code>AVCaptureSessionControlsDelegate</code> and add the required functions.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="// MARK: - AVCaptureSessionControlsDelegate
extension CameraViewModel: AVCaptureSessionControlsDelegate {
    
    func sessionControlsDidBecomeActive(_ session: AVCaptureSession) {
        return
    }
    
    func sessionControlsWillEnterFullscreenAppearance(_ session: AVCaptureSession) {
        return
    }
    
    func sessionControlsWillExitFullscreenAppearance(_ session: AVCaptureSession) {
        return
    }
    
    func sessionControlsDidBecomeInactive(_ session: AVCaptureSession) {
        return
    }
    
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">// MARK: - AVCaptureSessionControlsDelegate</span></span>
<span class="line"><span style="color: #81A1C1">extension</span><span style="color: #D8DEE9FF"> CameraViewModel</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">AVCaptureSessionControlsDelegate </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sessionControlsDidBecomeActive</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">: AVCaptureSession</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sessionControlsWillEnterFullscreenAppearance</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">: AVCaptureSession</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sessionControlsWillExitFullscreenAppearance</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">: AVCaptureSession</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sessionControlsDidBecomeInactive</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">: AVCaptureSession</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Even if you do nothing in the above functions, you have to implement the delegate in order to use the camera control features.</p>



<p>Now, we will initialize the system zoom control:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="let systemZoomSlider = AVCaptureSystemZoomSlider(device: backCamera) { zoomFactor in
    // Calculate and display a zoom value.
    let displayZoom = backCamera.displayVideoZoomFactorMultiplier * zoomFactor
    // Update the user interface.
    print(displayZoom)
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> systemZoomSlider </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureSystemZoomSlider</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">device</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> backCamera</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> zoomFactor </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// Calculate and display a zoom value.</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> displayZoom </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> backCamera.</span><span style="color: #D8DEE9">displayVideoZoomFactorMultiplier</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">*</span><span style="color: #D8DEE9FF"> zoomFactor</span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// Update the user interface.</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">displayZoom</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Here, we provide the camera device as the input. Within the code block, the system will run our code whenever the zoom level changes.</p>



<p>We will then remove all existing controls of the camera, and add the system zoom slider:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="/// remove existing camera controls first
self.session.controls.forEach({ self.session.removeControl($0) })

/// add new ones
let controlsToAdd: [AVCaptureControl] = [systemZoomSlider]

for control in controlsToAdd {
    if self.session.canAddControl(control) {
        self.session.addControl(control)
    }
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">/// remove existing camera controls first</span></span>
<span class="line"><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">controls</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">forEach</span><span style="color: #ECEFF4">({</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">removeControl</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$0</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">})</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">/// add new ones</span></span>
<span class="line"><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> controlsToAdd: </span><span style="color: #ECEFF4">[</span><span style="color: #D8DEE9FF">AVCaptureControl</span><span style="color: #ECEFF4">]</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> [systemZoomSlider]</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> control </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> controlsToAdd </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.session.</span><span style="color: #88C0D0">canAddControl</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">control</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">addControl</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">control</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Here is my updated <code>CameraViewModel</code></p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="// MARK: - Unified Camera ViewModel
class CameraViewModel: NSObject, ObservableObject {
    
    // Session states
    enum CameraSetupState {
        case idle
        case configured
        case permissionDenied
        case failed
    }
    
    @Published var setupState: CameraSetupState = .idle
    @Published var capturedPhoto: UIImage? = nil
    @Published var permissionGranted: Bool = false
    
    let session = AVCaptureSession()
    private let photoOutput = AVCapturePhotoOutput()
    private var videoInput: AVCaptureDeviceInput?
    
    // Dispatch queue for configuring the session
    private let configurationQueue = DispatchQueue(label: &quot;com.example.camera.config&quot;)
    private let cameraControlQueue = DispatchQueue(label: &quot;com.example.camera.control&quot;)
    
    override init() {
        super.init()
    }
    
    deinit {
        stopSession()
    }
    
    // MARK: - Public API
    
    /// Checks camera permissions and configures session if authorized.
    func requestAccessIfNeeded() { ... }
    
    /// Initiate photo capture.
    func capturePhoto() { ... }
    
    // MARK: - Session Configuration
    
    private func configureSessionIfIdle() { ... }
    
    private func addCameraInput() {
        do {
            guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera,
                                                           for: .video,
                                                           position: .back) else {
                print(&quot;CameraViewModel: Back camera is unavailable.&quot;)
                setupState = .idle
                session.commitConfiguration()
                return
            }
            
            let cameraInput = try AVCaptureDeviceInput(device: backCamera)
            if session.canAddInput(cameraInput) {
                session.addInput(cameraInput)
                videoInput = cameraInput
                DispatchQueue.main.async {
                    self.setupState = .configured
                }
            } else {
                print(&quot;CameraViewModel: Unable to add camera input to session.&quot;)
                setupState = .idle
                session.commitConfiguration()
            }
            
            // configure for camera control button
            let systemZoomSlider = AVCaptureSystemZoomSlider(device: backCamera) { zoomFactor in
                // Calculate and display a zoom value.
                let displayZoom = backCamera.displayVideoZoomFactorMultiplier * zoomFactor
                // Update the user interface.
                print(displayZoom)
            }
            
            /// remove existing camera controls first
            self.session.controls.forEach({ self.session.removeControl($0) })
            
            /// add new ones
            let controlsToAdd: [AVCaptureControl] = [systemZoomSlider]
            
            for control in controlsToAdd {
                if self.session.canAddControl(control) {
                    self.session.addControl(control)
                }
            }
            
            /// set delegate
            self.session.setControlsDelegate(self, queue: self.cameraControlQueue)
            //
        } catch {
            print(&quot;CameraViewModel: Error creating camera input - \(error)&quot;)
            setupState = .failed
            session.commitConfiguration()
        }
    }
    
    private func addPhotoOutput() { ... }
    
    private func startSessionIfReady() { ... }
    
    private func stopSession() { ... }
}

// MARK: - AVCaptureSessionControlsDelegate
extension CameraViewModel: AVCaptureSessionControlsDelegate {
    
    func sessionControlsDidBecomeActive(_ session: AVCaptureSession) {
        return
    }
    
    func sessionControlsWillEnterFullscreenAppearance(_ session: AVCaptureSession) {
        return
    }
    
    func sessionControlsWillExitFullscreenAppearance(_ session: AVCaptureSession) {
        return
    }
    
    func sessionControlsDidBecomeInactive(_ session: AVCaptureSession) {
        return
    }
    
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">// MARK: - Unified Camera ViewModel</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">CameraViewModel</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">NSObject</span><span style="color: #D8DEE9FF">, </span><span style="color: #8FBCBB; font-weight: bold">ObservableObject </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// Session states</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">enum</span><span style="color: #D8DEE9FF"> CameraSetupState </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">idle</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">configured</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">permissionDenied</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">failed</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #81A1C1">Published</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> setupState: CameraSetupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">idle</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #81A1C1">Published</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> capturedPhoto: UIImage</span><span style="color: #81A1C1">?</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">nil</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #81A1C1">Published</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> permissionGranted: </span><span style="color: #8FBCBB">Bool</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">false</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> session </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureSession</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> photoOutput </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCapturePhotoOutput</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> videoInput: AVCaptureDeviceInput</span><span style="color: #81A1C1">?</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// Dispatch queue for configuring the session</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> configurationQueue </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">DispatchQueue</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">label</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">com.example.camera.config</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> cameraControlQueue </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">DispatchQueue</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">label</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">com.example.camera.control</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">override</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">init</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">super</span><span style="color: #D8DEE9FF">.</span><span style="color: #81A1C1">init</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">deinit</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">stopSession</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// MARK: - Public API</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">/// Checks camera permissions and configures session if authorized.</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">requestAccessIfNeeded</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">...</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">/// Initiate photo capture.</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">capturePhoto</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">...</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// MARK: - Session Configuration</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">configureSessionIfIdle</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">...</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">addCameraInput</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">do</span><span style="color: #ECEFF4"> {</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> backCamera </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> AVCaptureDevice.</span><span style="color: #88C0D0">default</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">builtInWideAngleCamera</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                           </span><span style="color: #88C0D0">for</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">video</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                           </span><span style="color: #88C0D0">position</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">back</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: Back camera is unavailable.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                setupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">idle</span></span>
<span class="line"><span style="color: #D8DEE9FF">                session.</span><span style="color: #88C0D0">commitConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> cameraInput </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">try</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureDeviceInput</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">device</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> backCamera</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> session.</span><span style="color: #88C0D0">canAddInput</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">cameraInput</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                session.</span><span style="color: #88C0D0">addInput</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">cameraInput</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                videoInput </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> cameraInput</span></span>
<span class="line"><span style="color: #D8DEE9FF">                DispatchQueue.</span><span style="color: #D8DEE9">main</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">setupState</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">configured</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: Unable to add camera input to session.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                setupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">idle</span></span>
<span class="line"><span style="color: #D8DEE9FF">                session.</span><span style="color: #88C0D0">commitConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">// configure for camera control button</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> systemZoomSlider </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureSystemZoomSlider</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">device</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> backCamera</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> zoomFactor </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #ECEFF4">                </span><span style="color: #616E88">// Calculate and display a zoom value.</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> displayZoom </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> backCamera.</span><span style="color: #D8DEE9">displayVideoZoomFactorMultiplier</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">*</span><span style="color: #D8DEE9FF"> zoomFactor</span></span>
<span class="line"><span style="color: #ECEFF4">                </span><span style="color: #616E88">// Update the user interface.</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">displayZoom</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">/// remove existing camera controls first</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">controls</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">forEach</span><span style="color: #ECEFF4">({</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">removeControl</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$0</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">})</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">/// add new ones</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> controlsToAdd: </span><span style="color: #ECEFF4">[</span><span style="color: #D8DEE9FF">AVCaptureControl</span><span style="color: #ECEFF4">]</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> [systemZoomSlider]</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> control </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> controlsToAdd </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.session.</span><span style="color: #88C0D0">canAddControl</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">control</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">addControl</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">control</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">/// set delegate</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">setControlsDelegate</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">, </span><span style="color: #88C0D0">queue</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">cameraControlQueue</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">//</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">catch</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: Error creating camera input - </span><span style="color: #81A1C1">\(</span><span style="color: #A3BE8C">error</span><span style="color: #81A1C1">)</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            setupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">failed</span></span>
<span class="line"><span style="color: #D8DEE9FF">            session.</span><span style="color: #88C0D0">commitConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">addPhotoOutput</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">...</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">startSessionIfReady</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">...</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">stopSession</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">...</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// MARK: - AVCaptureSessionControlsDelegate</span></span>
<span class="line"><span style="color: #81A1C1">extension</span><span style="color: #D8DEE9FF"> CameraViewModel</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">AVCaptureSessionControlsDelegate </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sessionControlsDidBecomeActive</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">: AVCaptureSession</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sessionControlsWillEnterFullscreenAppearance</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">: AVCaptureSession</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sessionControlsWillExitFullscreenAppearance</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">: AVCaptureSession</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sessionControlsDidBecomeInactive</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">: AVCaptureSession</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Notice that I have added a <code>cameraControlQueue</code>, set the delegate, and set the system zoom slider within my <code>addCameraInput</code> function.</p>



<p>Now, if you run your program on your iPhone, you will notice that you can use the zoom slider within your own app:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="320" height="695" src="https://static-assets.mszpro.com/2024/12/ezgif-4-b3e64c3ed3.gif" alt="" class="wp-image-547"/></figure>



<h1 class="wp-block-heading">Add exposure control</h1>



<p>You can easily add control for exposure by adding one more camera control to the array:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="// configure for camera control button
let systemZoomSlider = AVCaptureSystemZoomSlider(device: backCamera) { zoomFactor in
    // Calculate and display a zoom value.
    let displayZoom = backCamera.displayVideoZoomFactorMultiplier * zoomFactor
    // Update the user interface.
    print(displayZoom)
}

let systemBiasSlider = AVCaptureSystemExposureBiasSlider(device: backCamera)

/// remove existing camera controls first
self.session.controls.forEach({ self.session.removeControl($0) })

/// add new ones
let controlsToAdd: [AVCaptureControl] = [systemZoomSlider, systemBiasSlider]

for control in controlsToAdd {
    if self.session.canAddControl(control) {
        self.session.addControl(control)
    }
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">// configure for camera control button</span></span>
<span class="line"><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> systemZoomSlider </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureSystemZoomSlider</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">device</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> backCamera</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> zoomFactor </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// Calculate and display a zoom value.</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> displayZoom </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> backCamera.</span><span style="color: #D8DEE9">displayVideoZoomFactorMultiplier</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">*</span><span style="color: #D8DEE9FF"> zoomFactor</span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// Update the user interface.</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">displayZoom</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> systemBiasSlider </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureSystemExposureBiasSlider</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">device</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> backCamera</span><span style="color: #ECEFF4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">/// remove existing camera controls first</span></span>
<span class="line"><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">controls</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">forEach</span><span style="color: #ECEFF4">({</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">removeControl</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$0</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">})</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">/// add new ones</span></span>
<span class="line"><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> controlsToAdd: </span><span style="color: #ECEFF4">[</span><span style="color: #D8DEE9FF">AVCaptureControl</span><span style="color: #ECEFF4">]</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> [systemZoomSlider, systemBiasSlider]</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> control </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> controlsToAdd </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.session.</span><span style="color: #88C0D0">canAddControl</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">control</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">addControl</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">control</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Now, when you launch the app and quickly double tap (not press) on the camera button, you will see 2 options, and you can switch to the exposure control (which is a slider provided by the system)</p>



<div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="471" height="1024" src="https://static-assets.mszpro.com/2024/12/IMG_3280-471x1024.png" alt="" class="wp-image-548" srcset="https://static-assets.mszpro.com/2024/12/IMG_3280-138x300.png 138w, https://static-assets.mszpro.com/2024/12/IMG_3280-471x1024.png 471w, https://static-assets.mszpro.com/2024/12/IMG_3280-768x1669.png 768w, https://static-assets.mszpro.com/2024/12/IMG_3280-707x1536.png 707w, https://static-assets.mszpro.com/2024/12/IMG_3280-943x2048.png 943w, https://static-assets.mszpro.com/2024/12/IMG_3280.png 1320w" sizes="auto, (max-width: 471px) 100vw, 471px" /></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="471" height="1024" src="https://static-assets.mszpro.com/2024/12/IMG_3279-471x1024.png" alt="" class="wp-image-549" srcset="https://static-assets.mszpro.com/2024/12/IMG_3279-138x300.png 138w, https://static-assets.mszpro.com/2024/12/IMG_3279-471x1024.png 471w, https://static-assets.mszpro.com/2024/12/IMG_3279-768x1669.png 768w, https://static-assets.mszpro.com/2024/12/IMG_3279-707x1536.png 707w, https://static-assets.mszpro.com/2024/12/IMG_3279-943x2048.png 943w, https://static-assets.mszpro.com/2024/12/IMG_3279.png 1320w" sizes="auto, (max-width: 471px) 100vw, 471px" /></figure>
</div>
</div>



<h1 class="wp-block-heading">Add custom slider</h1>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="502" src="https://static-assets.mszpro.com/2024/12/time-travel-with-mszpro-1024x502.png" alt="" class="wp-image-551" srcset="https://static-assets.mszpro.com/2024/12/time-travel-with-mszpro-300x147.png 300w, https://static-assets.mszpro.com/2024/12/time-travel-with-mszpro-1024x502.png 1024w, https://static-assets.mszpro.com/2024/12/time-travel-with-mszpro-768x376.png 768w, https://static-assets.mszpro.com/2024/12/time-travel-with-mszpro-1536x753.png 1536w, https://static-assets.mszpro.com/2024/12/time-travel-with-mszpro-2048x1004.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>You can initialize a custom slider, and use <code>setActionQueue</code> to get notified when the value changes.</p>



<p>Here is a little joke where you control the time (4th dimension( with my camera app (fancy right?)</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="let timeTravelSlider = AVCaptureSlider(&quot;MszProと時間旅行&quot;, symbolName: &quot;pawprint.fill&quot;, in: -10...10)
// Perform the slider's action on the session queue.
timeTravelSlider.setActionQueue(self.cameraControlQueue) { position in
    print(position)
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> timeTravelSlider </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureSlider</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">MszProと時間旅行</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF">, </span><span style="color: #88C0D0">symbolName</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">pawprint.fill</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF">, </span><span style="color: #88C0D0">in</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">-10</span><span style="color: #81A1C1">...</span><span style="color: #B48EAD">10</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #616E88">// Perform the slider&#39;s action on the session queue.</span></span>
<span class="line"><span style="color: #D8DEE9FF">timeTravelSlider.</span><span style="color: #88C0D0">setActionQueue</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">cameraControlQueue</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> position </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">position</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<h1 class="wp-block-heading">Add custom picker</h1>



<p>You can also allow the user to pick one of the many given options (for example, a list of filters)</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="502" src="https://static-assets.mszpro.com/2024/12/qiita-switch-1024x502.png" alt="" class="wp-image-552" srcset="https://static-assets.mszpro.com/2024/12/qiita-switch-300x147.png 300w, https://static-assets.mszpro.com/2024/12/qiita-switch-1024x502.png 1024w, https://static-assets.mszpro.com/2024/12/qiita-switch-768x376.png 768w, https://static-assets.mszpro.com/2024/12/qiita-switch-1536x753.png 1536w, https://static-assets.mszpro.com/2024/12/qiita-switch-2048x1004.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="let indexPicker = AVCaptureIndexPicker(&quot;Post to&quot;,
                                       symbolName: &quot;square.and.arrow.up&quot;,
                                       localizedIndexTitles: [
                                        &quot;Qiita&quot;,
                                        &quot;Twitter&quot;,
                                        &quot;SoraSNS&quot;
                                       ])
indexPicker.setActionQueue(self.cameraControlQueue) { value in
    print(value)
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> indexPicker </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureIndexPicker</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Post to</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                       </span><span style="color: #88C0D0">symbolName</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">square.and.arrow.up</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                       </span><span style="color: #88C0D0">localizedIndexTitles</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> [</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                        </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Qiita</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                        </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Twitter</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                        </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">SoraSNS</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                       ]</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">indexPicker.</span><span style="color: #88C0D0">setActionQueue</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">cameraControlQueue</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> value </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">value</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Notice, the value you get within the function is an index number.</p>



<h2 class="wp-block-heading">That is how you do it!</h2>



<p>Did you learn how to add custom camera control options within your own app? Here is the complete code:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff"><svg xmlns="http://www.w3.org/2000/svg" width="54" height="14" viewBox="0 0 54 14"><g fill="none" fill-rule="evenodd" transform="translate(1 1)"><circle cx="6" cy="6" r="6" fill="#FF5F56" stroke="#E0443E" stroke-width=".5"></circle><circle cx="26" cy="6" r="6" fill="#FFBD2E" stroke="#DEA123" stroke-width=".5"></circle><circle cx="46" cy="6" r="6" fill="#27C93F" stroke="#1AAB29" stroke-width=".5"></circle></g></svg></span><span role="button" tabindex="0" data-code="import SwiftUI
import AVFoundation
import Combine
import AVKit

// MARK: - SwiftUI Representable for Camera Preview
struct CameraLayerView: UIViewRepresentable {
    
    let cameraSession: AVCaptureSession
    
    func makeUIView(context: Context) -&gt; CameraContainerView {
        let container = CameraContainerView()
        container.backgroundColor = .black
        container.previewLayer.session = cameraSession
        container.previewLayer.videoGravity = .resizeAspect
        return container
    }
    
    func updateUIView(_ uiView: CameraContainerView, context: Context) {
        // No dynamic updates needed
    }
    
    // A UIView subclass that hosts an AVCaptureVideoPreviewLayer
    class CameraContainerView: UIView {
        
        override class var layerClass: AnyClass {
            AVCaptureVideoPreviewLayer.self
        }
        
        var previewLayer: AVCaptureVideoPreviewLayer {
            guard let layer = self.layer as? AVCaptureVideoPreviewLayer else {
                fatalError(&quot;CameraContainerView: Failed casting layer to AVCaptureVideoPreviewLayer.&quot;)
            }
            return layer
        }
    }
}

// MARK: - Unified Camera ViewModel
class CameraViewModel: NSObject, ObservableObject {
    
    // Session states
    enum CameraSetupState {
        case idle
        case configured
        case permissionDenied
        case failed
    }
    
    @Published var setupState: CameraSetupState = .idle
    @Published var capturedPhoto: UIImage? = nil
    @Published var permissionGranted: Bool = false
    
    let session = AVCaptureSession()
    private let photoOutput = AVCapturePhotoOutput()
    private var videoInput: AVCaptureDeviceInput?
    
    // Dispatch queue for configuring the session
    private let configurationQueue = DispatchQueue(label: &quot;com.example.camera.config&quot;)
    private let cameraControlQueue = DispatchQueue(label: &quot;com.example.camera.control&quot;)
    
    override init() {
        super.init()
    }
    
    deinit {
        stopSession()
    }
    
    // MARK: - Public API
    
    /// Checks camera permissions and configures session if authorized.
    func requestAccessIfNeeded() {
        let authStatus = AVCaptureDevice.authorizationStatus(for: .video)
        switch authStatus {
            case .authorized:
                permissionGranted = true
                configureSessionIfIdle()
            case .notDetermined:
                AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
                    guard let self = self else { return }
                    DispatchQueue.main.async {
                        if granted {
                            self.permissionGranted = true
                            self.configureSessionIfIdle()
                        } else {
                            self.setupState = .permissionDenied
                        }
                    }
                }
            default:
                // Denied or Restricted
                setupState = .permissionDenied
        }
    }
    
    /// Initiate photo capture.
    func capturePhoto() {
        guard setupState == .configured else { return }
        let settings = AVCapturePhotoSettings()
        photoOutput.capturePhoto(with: settings, delegate: self)
    }
    
    // MARK: - Session Configuration
    
    private func configureSessionIfIdle() {
        configurationQueue.async { [weak self] in
            guard let self = self, self.setupState == .idle else { return }
            
            self.session.beginConfiguration()
            
            self.session.sessionPreset = .photo
            
            self.addCameraInput()
            self.addPhotoOutput()
            
            // save configuration and start camera session
            self.session.commitConfiguration()
            self.startSessionIfReady()
        }
    }
    
    private func addCameraInput() {
        do {
            guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera,
                                                           for: .video,
                                                           position: .back) else {
                print(&quot;CameraViewModel: Back camera is unavailable.&quot;)
                setupState = .idle
                session.commitConfiguration()
                return
            }
            
            let cameraInput = try AVCaptureDeviceInput(device: backCamera)
            if session.canAddInput(cameraInput) {
                session.addInput(cameraInput)
                videoInput = cameraInput
                DispatchQueue.main.async {
                    self.setupState = .configured
                }
            } else {
                print(&quot;CameraViewModel: Unable to add camera input to session.&quot;)
                setupState = .idle
                session.commitConfiguration()
            }
            
            // configure for camera control button
            
            /// zoom slider
            let systemZoomSlider = AVCaptureSystemZoomSlider(device: backCamera) { zoomFactor in
                // Calculate and display a zoom value.
                let displayZoom = backCamera.displayVideoZoomFactorMultiplier * zoomFactor
                // Update the user interface.
                print(displayZoom)
            }
            
            /// exposure slider
            let systemBiasSlider = AVCaptureSystemExposureBiasSlider(device: backCamera)
            
            /// custom slider, learn time travel with MszPro
            let timeTravelSlider = AVCaptureSlider(&quot;MszProと時間旅行&quot;, symbolName: &quot;pawprint.fill&quot;, in: -10...10)
            // Perform the slider's action on the session queue.
            timeTravelSlider.setActionQueue(self.cameraControlQueue) { position in
                print(position)
            }
            
            /// custom index picker
            let indexPicker = AVCaptureIndexPicker(&quot;Post to&quot;,
                                                   symbolName: &quot;square.and.arrow.up&quot;,
                                                   localizedIndexTitles: [
                                                    &quot;Qiita&quot;,
                                                    &quot;Twitter&quot;,
                                                    &quot;SoraSNS&quot;
                                                   ])
            indexPicker.setActionQueue(self.cameraControlQueue) { value in
                print(value)
            }
            
            /// remove existing camera controls first
            self.session.controls.forEach({ self.session.removeControl($0) })
            
            /// add new ones
            let controlsToAdd: [AVCaptureControl] = [systemZoomSlider, systemBiasSlider, timeTravelSlider, indexPicker]
            
            for control in controlsToAdd {
                if self.session.canAddControl(control) {
                    self.session.addControl(control)
                }
            }
            
            /// set delegate
            self.session.setControlsDelegate(self, queue: self.cameraControlQueue)
            //
        } catch {
            print(&quot;CameraViewModel: Error creating camera input - \(error)&quot;)
            setupState = .failed
            session.commitConfiguration()
        }
    }
    
    private func addPhotoOutput() {
        guard session.canAddOutput(photoOutput) else {
            print(&quot;CameraViewModel: Cannot add photo output.&quot;)
            setupState = .failed
            session.commitConfiguration()
            return
        }
        session.addOutput(photoOutput)
        photoOutput.maxPhotoQualityPrioritization = .quality
        DispatchQueue.main.async {
            self.setupState = .configured
        }
    }
    
    private func startSessionIfReady() {
        guard setupState == .configured else { return }
        session.startRunning()
    }
    
    private func stopSession() {
        configurationQueue.async { [weak self] in
            guard let self = self else { return }
            if self.session.isRunning {
                self.session.stopRunning()
            }
        }
    }
}

// MARK: - AVCaptureSessionControlsDelegate
extension CameraViewModel: AVCaptureSessionControlsDelegate {
    
    func sessionControlsDidBecomeActive(_ session: AVCaptureSession) {
        return
    }
    
    func sessionControlsWillEnterFullscreenAppearance(_ session: AVCaptureSession) {
        return
    }
    
    func sessionControlsWillExitFullscreenAppearance(_ session: AVCaptureSession) {
        return
    }
    
    func sessionControlsDidBecomeInactive(_ session: AVCaptureSession) {
        return
    }
    
}

// MARK: - AVCapturePhotoCaptureDelegate
extension CameraViewModel: AVCapturePhotoCaptureDelegate {
    func photoOutput(_ output: AVCapturePhotoOutput,
                     didFinishProcessingPhoto photo: AVCapturePhoto,
                     error: Error?) {
        
        guard error == nil else {
            print(&quot;CameraViewModel: Error capturing photo - \(error!)&quot;)
            return
        }
        guard let photoData = photo.fileDataRepresentation() else {
            print(&quot;CameraViewModel: No photo data found.&quot;)
            return
        }
        self.capturedPhoto = UIImage(data: photoData)
    }
}

// MARK: - SwiftUI Main View
struct ContentView: View {
    
    @ObservedObject var viewModel = CameraViewModel()
    
    var body: some View {
        GeometryReader { _ in
            ZStack(alignment: .bottom) {
                CameraLayerView(cameraSession: viewModel.session)
                    .onAppear {
                        viewModel.requestAccessIfNeeded()
                    }
                    .edgesIgnoringSafeArea(.all)
                    .onCameraCaptureEvent() { event in
                        if event.phase == .began {
                            self.viewModel.capturePhoto()
                        }
                    }
                
                // Capture button
                VStack {
                    Spacer()
                    
                    Button {
                        viewModel.capturePhoto()
                    } label: {
                        Text(&quot;Take Photo&quot;)
                            .font(.headline)
                            .foregroundColor(.white)
                            .padding()
                            .background(Color.blue)
                            .cornerRadius(10)
                    }
                    .padding(.bottom, 20)
                }
                
                // Thumbnail of the captured photo
                if let image = viewModel.capturedPhoto {
                    Image(uiImage: image)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 120, height: 90)
                        .padding(.bottom, 80)
                }
            }
        }
        .task {
            let supportsCameraButton = self.viewModel.session.supportsControls
            
        }
    }
}

// MARK: - SwiftUI Preview
#Preview {
    ContentView()
}
" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> SwiftUI</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> AVFoundation</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> Combine</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> AVKit</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// MARK: - SwiftUI Representable for Camera Preview</span></span>
<span class="line"><span style="color: #81A1C1">struct</span><span style="color: #D8DEE9FF"> CameraLayerView</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">UIViewRepresentable </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> cameraSession: AVCaptureSession</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">makeUIView</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">context</span><span style="color: #D8DEE9FF">: Context</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">-&gt;</span><span style="color: #D8DEE9FF"> CameraContainerView </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> container </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">CameraContainerView</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">        container.</span><span style="color: #D8DEE9">backgroundColor</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">black</span></span>
<span class="line"><span style="color: #D8DEE9FF">        container.</span><span style="color: #D8DEE9">previewLayer</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> cameraSession</span></span>
<span class="line"><span style="color: #D8DEE9FF">        container.</span><span style="color: #D8DEE9">previewLayer</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">videoGravity</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">resizeAspect</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> container</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">updateUIView</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">uiView</span><span style="color: #D8DEE9FF">: CameraContainerView, </span><span style="color: #88C0D0">context</span><span style="color: #D8DEE9FF">: Context</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #ECEFF4">        </span><span style="color: #616E88">// No dynamic updates needed</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// A UIView subclass that hosts an AVCaptureVideoPreviewLayer</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">CameraContainerView</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">UIView </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">override</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> layerClass: </span><span style="color: #8FBCBB">AnyClass</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            AVCaptureVideoPreviewLayer.</span><span style="color: #81A1C1">self</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> previewLayer: AVCaptureVideoPreviewLayer </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> layer </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.layer </span><span style="color: #81A1C1">as?</span><span style="color: #D8DEE9FF"> AVCaptureVideoPreviewLayer </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">fatalError</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraContainerView: Failed casting layer to AVCaptureVideoPreviewLayer.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> layer</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// MARK: - Unified Camera ViewModel</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">CameraViewModel</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">NSObject</span><span style="color: #D8DEE9FF">, </span><span style="color: #8FBCBB; font-weight: bold">ObservableObject </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// Session states</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">enum</span><span style="color: #D8DEE9FF"> CameraSetupState </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">idle</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">configured</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">permissionDenied</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">failed</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #81A1C1">Published</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> setupState: CameraSetupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">idle</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #81A1C1">Published</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> capturedPhoto: UIImage</span><span style="color: #81A1C1">?</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">nil</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #81A1C1">Published</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> permissionGranted: </span><span style="color: #8FBCBB">Bool</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">false</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> session </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureSession</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> photoOutput </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCapturePhotoOutput</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> videoInput: AVCaptureDeviceInput</span><span style="color: #81A1C1">?</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// Dispatch queue for configuring the session</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> configurationQueue </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">DispatchQueue</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">label</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">com.example.camera.config</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> cameraControlQueue </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">DispatchQueue</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">label</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">com.example.camera.control</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">override</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">init</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">super</span><span style="color: #D8DEE9FF">.</span><span style="color: #81A1C1">init</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">deinit</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">stopSession</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// MARK: - Public API</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">/// Checks camera permissions and configures session if authorized.</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">requestAccessIfNeeded</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> authStatus </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> AVCaptureDevice.</span><span style="color: #88C0D0">authorizationStatus</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">for</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">video</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">switch</span><span style="color: #D8DEE9FF"> authStatus </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">authorized</span><span style="color: #81A1C1">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">                permissionGranted </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">configureSessionIfIdle</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">case</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">notDetermined</span><span style="color: #81A1C1">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">                AVCaptureDevice.</span><span style="color: #88C0D0">requestAccess</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">for</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">video</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> [</span><span style="color: #81A1C1">weak</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">] granted </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    DispatchQueue.</span><span style="color: #D8DEE9">main</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> granted </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">permissionGranted</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">configureSessionIfIdle</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">setupState</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">permissionDenied</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">default:</span></span>
<span class="line"><span style="color: #ECEFF4">                </span><span style="color: #616E88">// Denied or Restricted</span></span>
<span class="line"><span style="color: #D8DEE9FF">                setupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">permissionDenied</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">/// Initiate photo capture.</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">capturePhoto</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> setupState </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> .configured </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> settings </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCapturePhotoSettings</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">        photoOutput.</span><span style="color: #88C0D0">capturePhoto</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">with</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> settings, </span><span style="color: #88C0D0">delegate</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// MARK: - Session Configuration</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">configureSessionIfIdle</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        configurationQueue.</span><span style="color: #88C0D0">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> [</span><span style="color: #81A1C1">weak</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">] </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">, </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.setupState </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> .idle </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">beginConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">sessionPreset</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">photo</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">addCameraInput</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">addPhotoOutput</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">// save configuration and start camera session</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">commitConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">startSessionIfReady</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">addCameraInput</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">do</span><span style="color: #ECEFF4"> {</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> backCamera </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> AVCaptureDevice.</span><span style="color: #88C0D0">default</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">builtInWideAngleCamera</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                           </span><span style="color: #88C0D0">for</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">video</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                           </span><span style="color: #88C0D0">position</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">back</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: Back camera is unavailable.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                setupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">idle</span></span>
<span class="line"><span style="color: #D8DEE9FF">                session.</span><span style="color: #88C0D0">commitConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> cameraInput </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">try</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureDeviceInput</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">device</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> backCamera</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> session.</span><span style="color: #88C0D0">canAddInput</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">cameraInput</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                session.</span><span style="color: #88C0D0">addInput</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">cameraInput</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                videoInput </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> cameraInput</span></span>
<span class="line"><span style="color: #D8DEE9FF">                DispatchQueue.</span><span style="color: #D8DEE9">main</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">setupState</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">configured</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: Unable to add camera input to session.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                setupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">idle</span></span>
<span class="line"><span style="color: #D8DEE9FF">                session.</span><span style="color: #88C0D0">commitConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">// configure for camera control button</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">/// zoom slider</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> systemZoomSlider </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureSystemZoomSlider</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">device</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> backCamera</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> zoomFactor </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #ECEFF4">                </span><span style="color: #616E88">// Calculate and display a zoom value.</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> displayZoom </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> backCamera.</span><span style="color: #D8DEE9">displayVideoZoomFactorMultiplier</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">*</span><span style="color: #D8DEE9FF"> zoomFactor</span></span>
<span class="line"><span style="color: #ECEFF4">                </span><span style="color: #616E88">// Update the user interface.</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">displayZoom</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">/// exposure slider</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> systemBiasSlider </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureSystemExposureBiasSlider</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">device</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> backCamera</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">/// custom slider, learn time travel with MszPro</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> timeTravelSlider </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureSlider</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">MszProと時間旅行</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF">, </span><span style="color: #88C0D0">symbolName</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">pawprint.fill</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF">, </span><span style="color: #88C0D0">in</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">-10</span><span style="color: #81A1C1">...</span><span style="color: #B48EAD">10</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">// Perform the slider&#39;s action on the session queue.</span></span>
<span class="line"><span style="color: #D8DEE9FF">            timeTravelSlider.</span><span style="color: #88C0D0">setActionQueue</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">cameraControlQueue</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> position </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">position</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">/// custom index picker</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> indexPicker </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AVCaptureIndexPicker</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Post to</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                   </span><span style="color: #88C0D0">symbolName</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">square.and.arrow.up</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                   </span><span style="color: #88C0D0">localizedIndexTitles</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> [</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Qiita</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Twitter</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">SoraSNS</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                   ]</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            indexPicker.</span><span style="color: #88C0D0">setActionQueue</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">cameraControlQueue</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> value </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">value</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">/// remove existing camera controls first</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">controls</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">forEach</span><span style="color: #ECEFF4">({</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">removeControl</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$0</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">})</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">/// add new ones</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> controlsToAdd: </span><span style="color: #ECEFF4">[</span><span style="color: #D8DEE9FF">AVCaptureControl</span><span style="color: #ECEFF4">]</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> [systemZoomSlider, systemBiasSlider, timeTravelSlider, indexPicker]</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> control </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> controlsToAdd </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.session.</span><span style="color: #88C0D0">canAddControl</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">control</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">addControl</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">control</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">/// set delegate</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">setControlsDelegate</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">, </span><span style="color: #88C0D0">queue</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">cameraControlQueue</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #ECEFF4">            </span><span style="color: #616E88">//</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">catch</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: Error creating camera input - </span><span style="color: #81A1C1">\(</span><span style="color: #A3BE8C">error</span><span style="color: #81A1C1">)</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            setupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">failed</span></span>
<span class="line"><span style="color: #D8DEE9FF">            session.</span><span style="color: #88C0D0">commitConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">addPhotoOutput</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> session.</span><span style="color: #88C0D0">canAddOutput</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">photoOutput</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: Cannot add photo output.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            setupState </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">failed</span></span>
<span class="line"><span style="color: #D8DEE9FF">            session.</span><span style="color: #88C0D0">commitConfiguration</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        session.</span><span style="color: #88C0D0">addOutput</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">photoOutput</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">        photoOutput.</span><span style="color: #D8DEE9">maxPhotoQualityPrioritization</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">quality</span></span>
<span class="line"><span style="color: #D8DEE9FF">        DispatchQueue.</span><span style="color: #D8DEE9">main</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">setupState</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">configured</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">startSessionIfReady</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> setupState </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> .configured </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        session.</span><span style="color: #88C0D0">startRunning</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">stopSession</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        configurationQueue.</span><span style="color: #88C0D0">async</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> [</span><span style="color: #81A1C1">weak</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">] </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.session.isRunning </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">stopRunning</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// MARK: - AVCaptureSessionControlsDelegate</span></span>
<span class="line"><span style="color: #81A1C1">extension</span><span style="color: #D8DEE9FF"> CameraViewModel</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">AVCaptureSessionControlsDelegate </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sessionControlsDidBecomeActive</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">: AVCaptureSession</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sessionControlsWillEnterFullscreenAppearance</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">: AVCaptureSession</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sessionControlsWillExitFullscreenAppearance</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">: AVCaptureSession</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sessionControlsDidBecomeInactive</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">: AVCaptureSession</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// MARK: - AVCapturePhotoCaptureDelegate</span></span>
<span class="line"><span style="color: #81A1C1">extension</span><span style="color: #D8DEE9FF"> CameraViewModel</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">AVCapturePhotoCaptureDelegate </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">photoOutput</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">output</span><span style="color: #D8DEE9FF">: AVCapturePhotoOutput,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                     </span><span style="color: #88C0D0">didFinishProcessingPhoto</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">photo</span><span style="color: #D8DEE9FF">: AVCapturePhoto,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                     </span><span style="color: #88C0D0">error</span><span style="color: #D8DEE9FF">: </span><span style="color: #8FBCBB">Error</span><span style="color: #81A1C1">?</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> error </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">nil</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: Error capturing photo - </span><span style="color: #81A1C1">\(</span><span style="color: #A3BE8C">error</span><span style="color: #81A1C1">!)</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">guard</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> photoData </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> photo.</span><span style="color: #88C0D0">fileDataRepresentation</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">CameraViewModel: No photo data found.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">capturedPhoto</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">UIImage</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">data</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> photoData</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// MARK: - SwiftUI Main View</span></span>
<span class="line"><span style="color: #81A1C1">struct</span><span style="color: #D8DEE9FF"> ContentView</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">View </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #81A1C1">ObservedObject</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> viewModel </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">CameraViewModel</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> body: </span><span style="color: #81A1C1">some</span><span style="color: #D8DEE9FF"> View </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">GeometryReader</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> _ </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #88C0D0">ZStack</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">alignment</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">bottom</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">CameraLayerView</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">cameraSession</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> viewModel.</span><span style="color: #D8DEE9">session</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    .</span><span style="color: #88C0D0">onAppear</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        viewModel.</span><span style="color: #88C0D0">requestAccessIfNeeded</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    .</span><span style="color: #88C0D0">edgesIgnoringSafeArea</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">all</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    .</span><span style="color: #88C0D0">onCameraCaptureEvent</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> event </span><span style="color: #81A1C1">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> event.phase </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> .began </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">viewModel</span><span style="color: #D8DEE9FF">.</span><span style="color: #88C0D0">capturePhoto</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span></span>
<span class="line"><span style="color: #ECEFF4">                </span><span style="color: #616E88">// Capture button</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">VStack</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #88C0D0">Spacer</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #88C0D0">Button</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        viewModel.</span><span style="color: #88C0D0">capturePhoto</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">label</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #88C0D0">Text</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Take Photo</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            .</span><span style="color: #88C0D0">font</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">headline</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            .</span><span style="color: #88C0D0">foregroundColor</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">white</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            .</span><span style="color: #88C0D0">padding</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            .</span><span style="color: #88C0D0">background</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">Color.</span><span style="color: #D8DEE9">blue</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            .</span><span style="color: #88C0D0">cornerRadius</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">10</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    .</span><span style="color: #88C0D0">padding</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">bottom</span><span style="color: #D8DEE9FF">, </span><span style="color: #B48EAD">20</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span></span>
<span class="line"><span style="color: #ECEFF4">                </span><span style="color: #616E88">// Thumbnail of the captured photo</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> image </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> viewModel.capturedPhoto </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #88C0D0">Image</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">uiImage</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> image</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        .</span><span style="color: #88C0D0">resizable</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        .</span><span style="color: #88C0D0">aspectRatio</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">contentMode</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> .</span><span style="color: #D8DEE9">fit</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        .</span><span style="color: #88C0D0">frame</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">width</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">120</span><span style="color: #D8DEE9FF">, </span><span style="color: #88C0D0">height</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">90</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        .</span><span style="color: #88C0D0">padding</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">bottom</span><span style="color: #D8DEE9FF">, </span><span style="color: #B48EAD">80</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        .</span><span style="color: #88C0D0">task</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">let</span><span style="color: #D8DEE9FF"> supportsCameraButton </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">viewModel</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">session</span><span style="color: #D8DEE9FF">.</span><span style="color: #D8DEE9">supportsControls</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// MARK: - SwiftUI Preview</span></span>
<span class="line"><span style="color: #88C0D0">#Preview</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">ContentView</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span></code></pre></div>



<p>Enjoy!</p><p>The post <a href="https://mszpro.com/ios-camera-control-button">Adapt to physical camera control button within your own iOS app (for SwiftUI, Zoom, Exposure, & Custom Controls)</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Custom tab bar and side bar (Twitter iOS app like) built in SwiftUI</title>
		<link>https://mszpro.com/swiftui-custom-side-menu-bar</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Mon, 16 Dec 2024 12:31:13 +0000</pubDate>
				<category><![CDATA[SwiftUI]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=498</guid>

					<description><![CDATA[<p>In this article, we will implement a tab bar that has a user avatar (custom tab bar), and a slide to open side-bar within our SwiftUI application. Custom tab bar The benefit of using a custom tab bar is that you can add images, or menus (for example, allow the user to quickly switch to [&#8230;]</p>
<p>The post <a href="https://mszpro.com/swiftui-custom-side-menu-bar">Custom tab bar and side bar (Twitter iOS app like) built in SwiftUI</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></description>
										<content:encoded><![CDATA[<p id="4cd0">In this article, we will implement a tab bar that has a user avatar (custom tab bar), and a slide to open side-bar within our SwiftUI application.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="295" height="640" src="https://static-assets.mszpro.com/2024/12/image.gif" alt="" class="wp-image-500"/></figure>



<h1 class="wp-block-heading" id="8e7b">Custom tab bar</h1>



<p id="c33f">The benefit of using a custom tab bar is that you can add images, or menus (for example, allow the user to quickly switch to another account).</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="690" src="https://static-assets.mszpro.com/2024/12/image-10-1024x690.png" alt="" class="wp-image-501" srcset="https://static-assets.mszpro.com/2024/12/image-10-300x202.png 300w, https://static-assets.mszpro.com/2024/12/image-10-1024x690.png 1024w, https://static-assets.mszpro.com/2024/12/image-10-768x518.png 768w, https://static-assets.mszpro.com/2024/12/image-10.png 1206w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p id="9fbe">First, we will define which tab we have using an&nbsp;<code>enum</code>:</p>



<pre class="wp-block-code"><code>enum CurrentTab {<br>    case home<br>    case settings<br>    case profile<br>}</code></pre>



<p id="ee4c">We will create a variable to store and control which tab is currently selected:</p>



<pre class="wp-block-code"><code>@State private var currentTab: CurrentTab = .home</code></pre>



<p id="0e7d">For custom tab bar, we will still use a&nbsp;<code>TabView</code>&nbsp;, however, we will not provide the&nbsp;<code>.tabItem</code>&nbsp;for each of the view inside. This way, the system will not display the default tab bar.</p>



<pre class="wp-block-code"><code>TabView(selection: $currentTab) {<br>    <br>    Text("Home page")<br>        .tag(CurrentTab.home)<br>    <br>    Text("Settings page")<br>        .tag(CurrentTab.settings)<br>    <br>    Text("Profile page")<br>        .tag(CurrentTab.profile)<br>    <br>}</code></pre>



<p id="fd17">Then, we use a&nbsp;<code>ZStack</code>&nbsp;aligned to the bottom of the screen to add our custom tab bar on top of the current view.</p>



<pre class="wp-block-code"><code>var body: some View {<br>    <br>    ZStack(alignment: .bottom) {<br>        TabView(selection: $currentTab) {<br>            <br>            Text("Home page")<br>                .tag(CurrentTab.home)<br>            <br>            Text("Settings page")<br>                .tag(CurrentTab.settings)<br>            <br>            Text("Profile page")<br>                .tag(CurrentTab.profile)<br>            <br>        }<br>        <br>        floatingToolBar<br>        <br>    }<br>    <br>}</code></pre>



<p id="68ef">Now, I need to implement my tab bar. I first write a custom structure that stores the view of a single tab item within that tab bar:</p>



<pre class="wp-block-code"><code>func CustomTabItem(symbolName: String, isActive: Bool) -&gt; some View{<br>    HStack {<br>        Image(systemName: symbolName)<br>            .resizable()<br>            .foregroundColor(isActive ? .teal : .gray)<br>            .opacity(isActive ? 1 : 0.6)<br>            .frame(width: 22, height: 22)<br>    }<br>    .frame(maxWidth: .infinity)<br>    .frame(height: 38)<br>}</code></pre>



<p id="3473">Here, I am just showing an image, but you can use a&nbsp;<code>VStack</code>&nbsp;and put a text label at the bottom.</p>



<p id="7288">Now, I will implement my floating tab bar:</p>



<pre class="wp-block-code"><code>var floatingToolBar: some View {<br>    HStack {<br>        <br>        Spacer()<br>        <br>        Button {<br>            self.currentTab = .home<br>        } label: {<br>            CustomTabItem(<br>                symbolName: "house",<br>                isActive: self.currentTab == .home)<br>        }<br>        <br>        Spacer()<br>        <br>        Button {<br>            self.currentTab = .settings<br>        } label: {<br>            CustomTabItem(<br>                symbolName: "gear",<br>                isActive: self.currentTab == .settings)<br>        }<br>        <br>        Spacer()<br>        <br>        Button {<br>            self.currentTab = .profile<br>        } label: {<br>            AsyncImage(url: URL(string: "https://cdn.pixabay.com/photo/2024/03/07/10/38/simba-8618301_640.jpg")!) { loadedImage in<br>                loadedImage<br>                    .resizable()<br>                    .scaledToFit()<br>                    .clipShape(Circle())<br>                    .frame(maxWidth: .infinity)<br>                    .frame(height: 30)<br>                    .padding(2)<br>                    .background {<br>                        if self.currentTab == .profile {<br>                            Circle()<br>                                .stroke(.teal, lineWidth: 1)<br>                        }<br>                    }<br>            } placeholder: {<br>                ProgressView()<br>            }<br>            .frame(maxWidth: .infinity)<br>            .frame(height: 30)<br>        }<br>        <br>        Spacer()<br>        <br>    }<br>    .frame(maxWidth: .infinity)<br>    .padding(.top, 5)<br>    .padding(.horizontal, 20)<br>    .background(Color(uiColor: .systemGroupedBackground))<br>}</code></pre>



<p id="18c7">When the button is activated, it will have a blue color, otherwise, it will be gray.</p>



<p id="7328">Also, you can see, the benefit of the custom tab bar is that you can put any view components. For example, I can add a menu or an image (to show the user’s avatar image for example).</p>



<p id="ef92">You can also enable customization of the tab bar by allowing the user to turn on or off certain tabs.</p>



<p id="76ba">Now we have completed the custom tab bar. Let’s move on to the custom side bar.</p>



<h1 class="wp-block-heading" id="6ebe">Custom side bar</h1>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="824" height="1024" src="https://static-assets.mszpro.com/2024/12/image-11-824x1024.png" alt="" class="wp-image-502" srcset="https://static-assets.mszpro.com/2024/12/image-11-241x300.png 241w, https://static-assets.mszpro.com/2024/12/image-11-824x1024.png 824w, https://static-assets.mszpro.com/2024/12/image-11-768x955.png 768w, https://static-assets.mszpro.com/2024/12/image-11.png 1206w" sizes="auto, (max-width: 824px) 100vw, 824px" /></figure>



<p id="d119">To implement the custom side bar, we will use a&nbsp;<code>HStack</code>&nbsp;to put a menu on the left side of our main content.</p>



<p id="2cf7">We will use a&nbsp;<code>GeometryReader</code>&nbsp;on the outside to read the size of the screen. We will then calculate the width of the side bar.</p>



<pre class="wp-block-code"><code>var body: some View {<br>    GeometryReader { geometry in<br>        let sideBarWidth = geometry.size.width - 100<br><br>    }<br> }</code></pre>



<p id="e3bb">Then, we set the width of the side bar as the `sideBarWidth` and the width of the main content view the same as the screen width:</p>



<pre class="wp-block-code"><code>var body: some View {<br>    GeometryReader { geometry in<br>        let sideBarWidth = geometry.size.width - 100<br>        <br>        HStack(spacing: 0) {<br>            // Side Menu<br>            SideMenuView()<br>                .frame(width: sideBarWidth)<br>            <br>            // Main Content, which is the TabView from the above section<br>            MainContentView()<br>                .frame(width: geometry.size.width)<br>        }<br>    }<br>}</code></pre>



<p id="8171">But if we set the frame that way, the view will overflow the screen. So when the view is initially shown, we will set the offset on x axis, so the view initially only shows the content, and only if user scrolls that it will show the menu.</p>



<p id="3eac">In the below code, initially, the navigationState.offset value is 0, so the initial offset is set to just hide the menu. When the user starts drag gesture from left to right, it will update the offset x so it shows part of the side menu.</p>



<pre class="wp-block-code"><code>var body: some View {<br>    GeometryReader { geometry in<br>        let sideBarWidth = geometry.size.width - 100<br>        <br>        HStack(spacing: 0) {<br>            // Side Menu<br>            SideMenuView()<br>                .frame(width: sideBarWidth)<br>            <br>            // Main Content, which is the TabView from the above section<br>            MainContentView()<br>                .frame(width: geometry.size.width)<br>        }<br>+        .offset(x: -sideBarWidth + navigationState.offset)<br>    }<br>}</code></pre>



<p id="999e">Notice that it is important to define the frame&nbsp;<code>.frame(width: geometry.size.width)</code>&nbsp;for your main content view as well, so it fills the entire screen width.</p>



<p id="09cf">Then, we add the additional code for handling user’s gesture.</p>



<p id="9a3c">Here is the completed code:</p>



<pre class="wp-block-code"><code>struct ContentView: View {<br>    @StateObject private var navigationState = NavigationState()<br>    @GestureState private var gestureOffset: CGFloat = 0<br>    <br>    var body: some View {<br>        GeometryReader { geometry in<br>            let sideBarWidth = geometry.size.width - 100<br>            <br>            HStack(spacing: 0) {<br>                // Side Menu<br>                SideMenuView()<br>                    .frame(width: sideBarWidth)<br>                <br>                // Main Content, which is the TabView from the above section<br>                VStack {<br>                    HStack {<br>                        Rectangle().foregroundColor(.blue)<br>                    }<br>                }<br>                .frame(width: geometry.size.width)<br>            }<br>            .offset(x: -sideBarWidth + navigationState.offset)<br>            .gesture(<br>                DragGesture()<br>                    .updating($gestureOffset) { value, state, _ in  // Use the local gestureOffset<br>                        state = value.translation.width<br>                    }<br>                    .onEnded { value in<br>                        navigationState.handleGestureEnd(value: value, sideBarWidth: sideBarWidth)<br>                    }<br>            )<br>            .animation(.linear(duration: 0.15), value: navigationState.offset == 0)<br>            .onChange(of: navigationState.showMenu) { newValue in<br>                handleMenuVisibilityChange(sideBarWidth: sideBarWidth)<br>            }<br>            .onChange(of: gestureOffset) { newValue in  // Use the local gestureOffset<br>                handleGestureOffsetChange(sideBarWidth: sideBarWidth, gestureOffset: newValue)<br>            }<br>        }<br>    }<br>    <br>    private func handleMenuVisibilityChange(sideBarWidth: CGFloat) {<br>        if navigationState.showMenu &amp;&amp; navigationState.offset == 0 {<br>            navigationState.offset = sideBarWidth<br>            navigationState.lastStoredOffset = navigationState.offset<br>        }<br>        <br>        if !navigationState.showMenu &amp;&amp; navigationState.offset == sideBarWidth {<br>            navigationState.offset = 0<br>            navigationState.lastStoredOffset = 0<br>        }<br>    }<br>    <br>    private func handleGestureOffsetChange(sideBarWidth: CGFloat, gestureOffset: CGFloat) {<br>        if gestureOffset != 0 {<br>            let potentialOffset = navigationState.lastStoredOffset + gestureOffset<br>            if potentialOffset &lt; sideBarWidth &amp;&amp; potentialOffset &gt; 0 {<br>                navigationState.offset = potentialOffset<br>            } else if potentialOffset &lt; 0 {<br>                navigationState.offset = 0<br>            }<br>        }<br>    }<br>}<br><br>// MARK: - Navigation State<br>class NavigationState: ObservableObject {<br>    @Published var showMenu: Bool = false<br>    @Published var offset: CGFloat = 0<br>    @Published var lastStoredOffset: CGFloat = 0<br>    <br>    func handleGestureEnd(value: DragGesture.Value, sideBarWidth: CGFloat) {<br>        withAnimation(.spring(duration: 0.15)) {<br>            if value.translation.width &gt; 0 {<br>                // Handle opening gesture<br>                if value.translation.width &gt; sideBarWidth / 2 {<br>                    openMenu(sideBarWidth: sideBarWidth)<br>                } else if value.velocity.width &gt; 800 {<br>                    openMenu(sideBarWidth: sideBarWidth)<br>                } else if !showMenu {<br>                    closeMenu()<br>                }<br>            } else {<br>                // Handle closing gesture<br>                if -value.translation.width &gt; sideBarWidth / 2 {<br>                    closeMenu()<br>                } else {<br>                    guard showMenu else { return }<br>                    <br>                    if -value.velocity.width &gt; 800 {<br>                        closeMenu()<br>                    } else {<br>                        openMenu(sideBarWidth: sideBarWidth)<br>                    }<br>                }<br>            }<br>        }<br>        lastStoredOffset = offset<br>    }<br>    <br>    private func openMenu(sideBarWidth: CGFloat) {<br>        offset = sideBarWidth<br>        lastStoredOffset = sideBarWidth<br>        showMenu = true<br>    }<br>    <br>    private func closeMenu() {<br>        offset = 0<br>        showMenu = false<br>    }<br>}</code></pre>



<p id="1dff">We use the&nbsp;<code>handleGestureOffsetChange</code>&nbsp;function to check if the offset is valid (from zero up to the width of the menu). If the offset is valid, we update it to the&nbsp;<code>navigationState.offset</code>&nbsp;, which will then affect the above offset of the view.</p>



<p id="051d">In the&nbsp;<code>handleGestureEnd</code>&nbsp;function, we are running multiple checks to decide whether to open a menu if the user is swiping from left side to right side:</p>



<p id="8a7e"><code>if value.translation.width &gt; sideBarWidth / 2</code>&nbsp;this code means the user has slide the menu open.</p>



<p id="4035"><code>else if value.velocity.width &gt; 800</code>&nbsp;this means the user has not yet slide to the required width to open the menu, but the user is sliding the screen very fast, so we open the menu as well.</p>



<p id="7434">The logic for closing the menu is the same, which works by checking the distance user has swiped, and check the velocity.</p>



<p id="146c">Now, you have a side bar menu that can be open and close like a drawer.</p>



<h1 class="wp-block-heading" id="4d72">Complete code for side menu and custom tool bar</h1>



<pre class="wp-block-code"><code>//<br>//  ContentView.swift<br>//  TabBarSideBarDemo<br>//<br>//  Created by msz on 2024/12/09.<br>//<br><br>import SwiftUI<br><br>enum CurrentTab {<br>    case home<br>    case settings<br>    case profile<br>}<br><br>struct ContentView: View {<br>    @StateObject private var navigationState = NavigationState()<br>    @GestureState private var gestureOffset: CGFloat = 0<br>    @State private var currentTab: CurrentTab = .home<br>    <br>    var body: some View {<br>        GeometryReader { geometry in<br>            let sideBarWidth = geometry.size.width - 100<br>            <br>            HStack(spacing: 0) {<br>                // Side Menu<br>                SideMenuView()<br>                    .frame(width: sideBarWidth)<br>                <br>                // Main Content, which is the TabView from the above section<br>                ZStack(alignment: .bottom) {<br>                    TabView(selection: $currentTab) {<br>                        <br>                        Text("Home page")<br>                            .tag(CurrentTab.home)<br>                        <br>                        Text("Settings page")<br>                            .tag(CurrentTab.settings)<br>                        <br>                        Text("Profile page")<br>                            .tag(CurrentTab.profile)<br>                        <br>                    }<br>                    <br>                    floatingToolBar<br>                }<br>                .frame(width: geometry.size.width)<br>            }<br>            .offset(x: -sideBarWidth + navigationState.offset)<br>            .gesture(<br>                DragGesture()<br>                    .updating($gestureOffset) { value, state, _ in  // Use the local gestureOffset<br>                        state = value.translation.width<br>                    }<br>                    .onEnded { value in<br>                        navigationState.handleGestureEnd(value: value, sideBarWidth: sideBarWidth)<br>                    }<br>            )<br>            .animation(.linear(duration: 0.15), value: navigationState.offset == 0)<br>            .onChange(of: navigationState.showMenu) { newValue in<br>                handleMenuVisibilityChange(sideBarWidth: sideBarWidth)<br>            }<br>            .onChange(of: gestureOffset) { newValue in  // Use the local gestureOffset<br>                handleGestureOffsetChange(sideBarWidth: sideBarWidth, gestureOffset: newValue)<br>            }<br>        }<br>    }<br>    <br>    // MARK: Custom side menu<br>    <br>    private func handleMenuVisibilityChange(sideBarWidth: CGFloat) {<br>        if navigationState.showMenu &amp;&amp; navigationState.offset == 0 {<br>            navigationState.offset = sideBarWidth<br>            navigationState.lastStoredOffset = navigationState.offset<br>        }<br>        <br>        if !navigationState.showMenu &amp;&amp; navigationState.offset == sideBarWidth {<br>            navigationState.offset = 0<br>            navigationState.lastStoredOffset = 0<br>        }<br>    }<br>    <br>    private func handleGestureOffsetChange(sideBarWidth: CGFloat, gestureOffset: CGFloat) {<br>        if gestureOffset != 0 {<br>            let potentialOffset = navigationState.lastStoredOffset + gestureOffset<br>            if potentialOffset &lt; sideBarWidth &amp;&amp; potentialOffset &gt; 0 {<br>                navigationState.offset = potentialOffset<br>            } else if potentialOffset &lt; 0 {<br>                navigationState.offset = 0<br>            }<br>        }<br>    }<br>    <br>    // MARK: Custom tab bar<br>    <br>    var floatingToolBar: some View {<br>        HStack {<br>            <br>            Spacer()<br>            <br>            Button {<br>                self.currentTab = .home<br>            } label: {<br>                CustomTabItem(<br>                    symbolName: "house",<br>                    isActive: self.currentTab == .home)<br>            }<br>            <br>            Spacer()<br>            <br>            Button {<br>                self.currentTab = .settings<br>            } label: {<br>                CustomTabItem(<br>                    symbolName: "gear",<br>                    isActive: self.currentTab == .settings)<br>            }<br>            <br>            Spacer()<br>            <br>            Button {<br>                self.currentTab = .profile<br>            } label: {<br>                AsyncImage(url: URL(string: "https://cdn.pixabay.com/photo/2024/03/07/10/38/simba-8618301_640.jpg")!) { loadedImage in<br>                    loadedImage<br>                        .resizable()<br>                        .scaledToFit()<br>                        .clipShape(Circle())<br>                        .frame(maxWidth: .infinity)<br>                        .frame(height: 30)<br>                        .padding(2)<br>                        .background {<br>                            if self.currentTab == .profile {<br>                                Circle()<br>                                    .stroke(.teal, lineWidth: 1)<br>                            }<br>                        }<br>                } placeholder: {<br>                    ProgressView()<br>                }<br>                .frame(maxWidth: .infinity)<br>                .frame(height: 30)<br>            }<br>            <br>            Spacer()<br>            <br>        }<br>        .frame(maxWidth: .infinity)<br>        .padding(.top, 5)<br>        .padding(.horizontal, 20)<br>        .background(Color(uiColor: .systemGroupedBackground))<br>    }<br>    <br>    func CustomTabItem(symbolName: String, isActive: Bool) -&gt; some View{<br>        HStack {<br>            Image(systemName: symbolName)<br>                .resizable()<br>                .foregroundColor(isActive ? .teal : .gray)<br>                .opacity(isActive ? 1 : 0.6)<br>                .frame(width: 22, height: 22)<br>        }<br>        .frame(maxWidth: .infinity)<br>        .frame(height: 38)<br>    }<br>}<br><br>// MARK: - Navigation State<br>class NavigationState: ObservableObject {<br>    @Published var showMenu: Bool = false<br>    @Published var offset: CGFloat = 0<br>    @Published var lastStoredOffset: CGFloat = 0<br>    <br>    func handleGestureEnd(value: DragGesture.Value, sideBarWidth: CGFloat) {<br>        withAnimation(.spring(duration: 0.15)) {<br>            if value.translation.width &gt; 0 {<br>                // Handle opening gesture<br>                if value.translation.width &gt; sideBarWidth / 2 {<br>                    openMenu(sideBarWidth: sideBarWidth)<br>                } else if value.velocity.width &gt; 800 {<br>                    openMenu(sideBarWidth: sideBarWidth)<br>                } else if !showMenu {<br>                    closeMenu()<br>                }<br>            } else {<br>                // Handle closing gesture<br>                if -value.translation.width &gt; sideBarWidth / 2 {<br>                    closeMenu()<br>                } else {<br>                    guard showMenu else { return }<br>                    <br>                    if -value.velocity.width &gt; 800 {<br>                        closeMenu()<br>                    } else {<br>                        openMenu(sideBarWidth: sideBarWidth)<br>                    }<br>                }<br>            }<br>        }<br>        lastStoredOffset = offset<br>    }<br>    <br>    private func openMenu(sideBarWidth: CGFloat) {<br>        offset = sideBarWidth<br>        lastStoredOffset = sideBarWidth<br>        showMenu = true<br>    }<br>    <br>    private func closeMenu() {<br>        offset = 0<br>        showMenu = false<br>    }<br>    <br>}<br><br>// MARK: - Preview<br>#Preview {<br>    ContentView()<br>        .environmentObject(NavigationState())<br>}</code></pre>



<h1 class="wp-block-heading" id="d25c">Thank you for reading!</h1>



<p id="77d2">The code in this article is available at:&nbsp;<a href="https://github.com/mszpro/Custom-Side-Tab-Bar" rel="noreferrer noopener" target="_blank">https://github.com/mszpro/Custom-Side-Tab-Bar</a></p>



<p id="928d">Japanese version:&nbsp;<a href="https://qiita.com/mashunzhe/items/17fa31267e69e2fd8cd8" rel="noreferrer noopener" target="_blank">https://qiita.com/mashunzhe/items/17fa31267e69e2fd8cd8</a></p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="586" height="488" src="https://static-assets.mszpro.com/2024/12/image-12.png" alt="" class="wp-image-503" srcset="https://static-assets.mszpro.com/2024/12/image-12-300x250.png 300w, https://static-assets.mszpro.com/2024/12/image-12.png 586w" sizes="auto, (max-width: 586px) 100vw, 586px" /></figure>



<p id="5c83">Follow me on Twitter:&nbsp;<a href="https://twitter.com/mszpro" rel="noreferrer noopener" target="_blank">https://twitter.com/mszpro</a></p>



<p id="4d88">Subscribe on Youtube:&nbsp;<a href="https://www.youtube.com/@MszPro6" rel="noreferrer noopener" target="_blank">https://www.youtube.com/@MszPro6</a></p>



<p id="1975">Mastodon, Misskey: @me@mszpro.com</p>



<p id="ca55">Bluesky: @mszpro.com</p>



<p id="1d32">Website:&nbsp;<a href="https://mszpro.com/" rel="noreferrer noopener" target="_blank">https://mszpro.com</a></p>



<p id="71b1">SoraSNS for Mastodon, Misskey, Bluesky, Nostr all in one:&nbsp;<a href="https://mszpro.com/sorasns" rel="noreferrer noopener" target="_blank">https://mszpro.com/sorasns</a></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="471" height="1024" src="https://static-assets.mszpro.com/2024/12/image-13-471x1024.png" alt="" class="wp-image-504" srcset="https://static-assets.mszpro.com/2024/12/image-13-138x300.png 138w, https://static-assets.mszpro.com/2024/12/image-13-471x1024.png 471w, https://static-assets.mszpro.com/2024/12/image-13-768x1670.png 768w, https://static-assets.mszpro.com/2024/12/image-13-706x1536.png 706w, https://static-assets.mszpro.com/2024/12/image-13-942x2048.png 942w, https://static-assets.mszpro.com/2024/12/image-13.png 1206w" sizes="auto, (max-width: 471px) 100vw, 471px" /></figure><p>The post <a href="https://mszpro.com/swiftui-custom-side-menu-bar">Custom tab bar and side bar (Twitter iOS app like) built in SwiftUI</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Free translation API — using the iOS 18&#8217;s system Translation framework in your app (SwiftUI or UIKit)</title>
		<link>https://mszpro.com/ios-system-translate</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Mon, 16 Dec 2024 12:26:05 +0000</pubDate>
				<category><![CDATA[iOS 18]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<category><![CDATA[UIKit]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=486</guid>

					<description><![CDATA[<p>If you offer contents within your app and are targeting users around the world, you probably want translation feature. User submitted texts can be in all kinds of languages, and providing a translation feature will enrich your user experience. For example, in my Mastodon, Misskey, Bluesky, Nostr all in one client SoraSNS, I used the [&#8230;]</p>
<p>The post <a href="https://mszpro.com/ios-system-translate">Free translation API — using the iOS 18’s system Translation framework in your app (SwiftUI or UIKit)</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></description>
										<content:encoded><![CDATA[<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1400" height="457" src="https://static-assets.mszpro.com/2024/12/1uomB_nQRMRCAAHznVcfAEA@2x.png" alt="" class="wp-image-495" srcset="https://static-assets.mszpro.com/2024/12/1uomB_nQRMRCAAHznVcfAEA@2x-300x98.png 300w, https://static-assets.mszpro.com/2024/12/1uomB_nQRMRCAAHznVcfAEA@2x-1024x334.png 1024w, https://static-assets.mszpro.com/2024/12/1uomB_nQRMRCAAHznVcfAEA@2x-768x251.png 768w, https://static-assets.mszpro.com/2024/12/1uomB_nQRMRCAAHznVcfAEA@2x.png 1400w" sizes="auto, (max-width: 1400px) 100vw, 1400px" /><figcaption class="wp-element-caption">Photo from Apple documentation</figcaption></figure>



<p id="788b">If you offer contents within your app and are targeting users around the world, you probably want translation feature. User submitted texts can be in all kinds of languages, and providing a translation feature will enrich your user experience.</p>



<p id="284e">For example, in my Mastodon, Misskey, Bluesky, Nostr all in one client SoraSNS, I used the similar technique described in this article for free translation API.</p>



<p id="6f54">Using online API provided by companies like Google or DeepL can incur a fee, which indie developers like myself find it difficult.</p>



<h1 class="wp-block-heading" id="b45f">The answer: The Translation Framework</h1>



<p id="8c33">Ever since iOS 14 in 2020, Apple has introduced the system Translation app. It is a very simple yet elegant app with just a single text box. What’s more, Apple’s translation app allows you to download for local translation as well.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1400" height="829" src="https://static-assets.mszpro.com/2024/12/1bYNmmm5hc5RIVzl7NtkNyA.png" alt="" class="wp-image-490" srcset="https://static-assets.mszpro.com/2024/12/1bYNmmm5hc5RIVzl7NtkNyA-300x178.png 300w, https://static-assets.mszpro.com/2024/12/1bYNmmm5hc5RIVzl7NtkNyA-1024x606.png 1024w, https://static-assets.mszpro.com/2024/12/1bYNmmm5hc5RIVzl7NtkNyA-768x455.png 768w, https://static-assets.mszpro.com/2024/12/1bYNmmm5hc5RIVzl7NtkNyA.png 1400w" sizes="auto, (max-width: 1400px) 100vw, 1400px" /></figure>



<p id="0e39">In iOS 18, Apple has opened the translation API, so your app can use it to translate content.</p>



<p id="4112">Enough story telling. Let’s get started!</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p id="e2fc"><strong>Notice: You have to test these APIs on real devices. The Translation feature does not work in simulator!<em>&nbsp;(wired-ly sometimes the pre-build UI does work in simulator LOL)</em></strong></p>
</blockquote>



<h1 class="wp-block-heading" id="3e18">Present pre-built translation UI in SwiftUI</h1>



<p id="0c87">Your SwiftUI app can present a pre-built Apple translate app popup, within your own app.</p>



<p id="bf35">First, import the&nbsp;<code>Translation</code>&nbsp;framework. You can also use the&nbsp;<code>#if canImport(Translation)</code>statement to check if the system supports it. For example, if the user is running iOS 18 and up, provide the translation feature; if the user is running iOS 17 and below, fallback to an online translation service or hide the translation button.</p>



<pre class="wp-block-code"><code>#if canImport(Translation)<br>import Translation<br>#endif</code></pre>



<p id="dd19">Then, create a&nbsp;<code>@State</code>&nbsp;variable that controls when the translation dialog is shown. Initially, it is&nbsp;<code>false</code>&nbsp;and you set it to&nbsp;<code>true</code>&nbsp;once you want to present the translated result:</p>



<pre class="wp-block-code"><code>@State private var isTranslationShown: Bool = false</code></pre>



<p id="6e30">Then, attach the&nbsp;<code>translationPresentation</code>&nbsp;view modifier to your view component, with the&nbsp;<code>isPresented</code>&nbsp;parameter set to the binding of the variable that controls whether it is shown or not, and the&nbsp;<code>text</code>&nbsp;parameter set to the text you want to translate.</p>



<pre class="wp-block-code"><code>Form {<br>    // ... //<br>}<br>#if canImport(Translation)<br>.translationPresentation(isPresented: $isTranslationShown,<br>                         text: self.sourceText)<br>#endif</code></pre>



<p id="a752">Here is the full example code:</p>



<pre class="wp-block-code"><code>import SwiftUI<br><br>#if canImport(Translation)<br>import Translation<br>#endif<br><br>struct PopupTranslation: View {<br>    <br>    @State private var sourceText = "Hello, World! This is a test."<br>    <br>    @State private var isTranslationShown: Bool = false<br>    <br>    var body: some View {<br>        <br>        NavigationStack {<br>            Form {<br>                <br>                Section {<br>                    Label("Source text", systemImage: "globe")<br>                    <br>                    TextField("What do you want to translate?",<br>                              text: $sourceText,<br>                              axis: .vertical)<br>                }<br>                <br>            }<br>            .toolbar {<br>                ToolbarItem(placement: .topBarTrailing) {<br>                    Button("Translate") {<br>                        self.isTranslationShown = true<br>                    }<br>                }<br>            }<br>#if canImport(Translation)<br>            .translationPresentation(isPresented: $isTranslationShown,<br>                                     text: self.sourceText)<br>#endif<br>        }<br>        <br>    }<br>    <br>}<br><br>#Preview {<br>    PopupTranslation()<br>}</code></pre>



<p id="53ca">Now, if you run it, you will get a simple translation popup when the user presses the translate button:</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="295" height="640" src="https://static-assets.mszpro.com/2024/12/1qErPBfjANhIwW41IJD8_dQ.gif" alt="" class="wp-image-494"/></figure>



<p id="cf9e">You can also add a button within the present popup so user can quickly provide the translated text to your app:</p>



<pre class="wp-block-code"><code>//<br>//  PopupTranslation.swift<br>//  iOSTranslationVideo<br>//<br>//  Created by msz on 2024/12/01.<br>//<br><br>import SwiftUI<br><br>#if canImport(Translation)<br>import Translation<br>#endif<br><br>struct PopupTranslation: View {<br>    <br>    @State private var sourceText = "Hello, World!"<br>+    @State private var targetText = ""<br>    <br>    @State private var isTranslationShown: Bool = false<br>    <br>    var body: some View {<br>        <br>        NavigationStack {<br>            Form {<br>                <br>                Section {<br>                    Label("Source text", systemImage: "globe")<br>                    <br>                    TextField("What do you want to translate?",<br>                              text: $sourceText,<br>                              axis: .vertical)<br>                }<br>                <br>+                Section {<br>+                    Label("Translated text", systemImage: "globe")                    <br>+                    Text(targetText)<br>+                }<br>                <br>            }<br>            .toolbar {<br>                ToolbarItem(placement: .topBarTrailing) {<br>                    Button("Translate") {<br>                        self.isTranslationShown = true<br>                    }<br>                }<br>            }<br>#if canImport(Translation)<br>            .translationPresentation(isPresented: $isTranslationShown,<br>                                     text: self.sourceText)<br>+            { newString in<br>+                self.targetText = newString<br>+            }<br>#endif<br>        }<br>        <br>    }<br>    <br>}<br><br>#Preview {<br>    PopupTranslation()<br>}</code></pre>



<p id="04c7">In the above code, we added a code block for the&nbsp;<code>translationPresentation</code>view modifier. We then set the result text to the&nbsp;<code>targetText</code>&nbsp;variable of the app.</p>



<p id="5309">This will not automatically provide the translated result to the app. Instead, user will see a button called&nbsp;<code>Replace with translation</code></p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="295" height="640" src="https://static-assets.mszpro.com/2024/12/1c8eulmzf4daXMw3Rriq0zA.gif" alt="" class="wp-image-491"/></figure>



<p id="0cb6">The benefit of the above is that it uses system pre-designed UI, and you do not need to provide the source and target language, all are auto configured and detected by the system.</p>



<p id="79f3">But if you want to get the translated result programmatically, read on!</p>



<h1 class="wp-block-heading" id="fd26">Check if language is supported by iOS system</h1>



<p id="bffd">iOS system offers translation service for some popular languages. To check which language pair is available, you can use</p>



<pre class="wp-block-code"><code>func checkSpecificLanguagePairs() async {<br>    let availability = LanguageAvailability()<br>    <br>    // English to Japanese<br>    let english = Locale.Language(identifier: "en")<br>    let japanese = Locale.Language(identifier: "ja")<br>    let statusEnJa = await availability.status(from: english, to: japanese)<br>    print("English to Japanese: \(statusDescription(statusEnJa))")<br>    <br>    // English to Simplified Chinese<br>    let chinese = Locale.Language(identifier: "zh-Hans")<br>    let statusEnCh = await availability.status(from: english, to: chinese)<br>    print("English to Simplified Chinese: \(statusDescription(statusEnCh))")<br>    <br>    <br>    // English to German<br>    let german = Locale.Language(identifier: "de")<br>    let statusEnDe = await availability.status(from: english, to: german)<br>    print("English to German: \(statusDescription(statusEnDe))")<br>}<br><br>// Helper function to describe the status<br>func statusDescription(_ status: LanguageAvailability.Status) -&gt; String {<br>    switch status {<br>        case .installed:<br>            return "Translation installed and ready to use."<br>        case .supported:<br>            return "Translation supported but requires download of translation model."<br>        case .unsupported:<br>            return "Translation unsupported between the given language pair."<br>        @unknown default:<br>            return "Unknown status"<br>    }<br>}</code></pre>



<p id="60a5">Now, if the returned status is&nbsp;<code>installed</code>, it means you can translate normally. If it is&nbsp;<code>unsupported</code>&nbsp;, it means iOS does not support translation of that language peer. If it is&nbsp;<code>supported</code>&nbsp;but not&nbsp;<code>installed</code>&nbsp;it means the iOS system has not yet downloaded the files necessary for this translation.</p>



<p id="176e">These translation model files only need to be downloaded once every device.</p>



<h2 class="wp-block-heading" id="e736">Bonus topic: checking the language of a text</h2>



<p id="3994">Your app can actually detect the language of a given text by using&nbsp;<code>NaturalLanguage</code>&nbsp;framework</p>



<pre class="wp-block-code"><code>import NaturalLanguage<br><br>static func detectLanguage(for string: String) -&gt; String? {<br>    let recognizer = NLLanguageRecognizer()<br>    recognizer.processString(string)<br>    guard let languageCode = recognizer.dominantLanguage?.rawValue else {<br>        return nil<br>    }<br>    return languageCode<br>}</code></pre>



<h1 class="wp-block-heading" id="86af">Download translation models</h1>



<p id="d05b">You can ask iOS to present the download dialog for the translation files.</p>



<pre class="wp-block-code"><code>struct TranslationModelDownloader: View {<br>    <br>    var configuration: TranslationSession.Configuration {<br>        TranslationSession.Configuration(<br>            source: Locale.Language(identifier: "en"),<br>            target: Locale.Language(identifier: "ja")<br>        )<br>    }<br>    <br>    var body: some View {<br>        NavigationView {<br>            Text("Download translation files between \(configuration.source?.minimalIdentifier ?? "?") and \(configuration.target?.minimalIdentifier ?? "?")")<br>            .translationTask(configuration) { session in<br>                do {<br>                    try await session.prepareTranslation()<br>                } catch {<br>                    // Handle any errors.<br>                    print("Error downloading translation: \(error)")<br>                }<br>            }<br>        }<br>    }<br>}</code></pre>



<p id="f2a5">In the above code, you will attach the&nbsp;<code>translationTask</code>&nbsp;view modifier to your SwiftUI view. You will define the language configuration (source language and target language) within the&nbsp;<code>configuration</code>&nbsp;parameter. Then, you will call&nbsp;<code>session.prepareTranslation()</code>&nbsp;within the translation task view modifier.</p>



<p id="cd84">When the view shows up, it will present a system dialog for downloading translation files.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="600" height="1303" src="https://static-assets.mszpro.com/2024/12/1ySRkfOVcJTaSARhg5cwrvw.gif" alt="" class="wp-image-492"/></figure>



<p id="c5c0">Here is a full SwiftUI demo code for checking available languages and downloading the models:</p>



<pre class="wp-block-code"><code>//<br>//  LanguageAvailabilityChecker.swift<br>//  iOSTranslationVideo<br>//<br>//  Created by msz on 2024/12/01.<br>//<br><br>import SwiftUI<br>import Translation<br><br>fileprivate class ViewModel: ObservableObject {<br>    @Published var sourceLanguage: Locale.Language = Locale.current.language<br>    @Published var targetLanguage: Locale.Language = Locale.current.language<br>    <br>    @Published var languageStatus: LanguageAvailability.Status = .unsupported<br>    <br>    @Published var sourceFilter: String = "English"<br>    @Published var targetFilter: String = "German"<br>    <br>    let languages: &#91;Locale.Language]<br>    <br>    init() {<br>        // Initialize the list of available languages<br>        let languageCodes = Locale.LanguageCode.isoLanguageCodes<br>        self.languages = languageCodes.compactMap { Locale.Language(languageCode: $0) }<br>    }<br>    <br>    func displayName(for language: Locale.Language) -&gt; String {<br>        guard let languageCode = language.languageCode?.identifier else {<br>            return language.maximalIdentifier<br>        }<br>        return Locale.current.localizedString(forLanguageCode: languageCode) ?? languageCode<br>    }<br>    <br>    var filteredSourceLanguages: &#91;Locale.Language] {<br>        if sourceFilter.isEmpty {<br>            return languages<br>        } else {<br>            return languages.filter {<br>                displayName(for: $0).localizedCaseInsensitiveContains(sourceFilter)<br>            }<br>        }<br>    }<br>    <br>    var filteredTargetLanguages: &#91;Locale.Language] {<br>        if targetFilter.isEmpty {<br>            return languages<br>        } else {<br>            return languages.filter {<br>                displayName(for: $0).localizedCaseInsensitiveContains(targetFilter)<br>            }<br>        }<br>    }<br>    <br>    func checkLanguageSupport() async {<br>        let availability = LanguageAvailability()<br>        let status = await availability.status(from: sourceLanguage, to: targetLanguage)<br>        <br>        DispatchQueue.main.async {<br>            self.languageStatus = status<br>        }<br>    }<br>}<br><br><br>struct LanguageAvailabilityChecker: View {<br>    @StateObject fileprivate var viewModel = ViewModel()<br>    <br>    var body: some View {<br>        Form {<br>            // Source Language Section<br>            Section("Source Language") {<br>                TextField("Filter languages", text: $viewModel.sourceFilter)<br>                    .padding(.vertical, 4)<br>                <br>                Picker("Select Source Language", selection: $viewModel.sourceLanguage) {<br>                    ForEach(viewModel.filteredSourceLanguages, id: \.maximalIdentifier) { language in<br>                        Button {} label: {<br>                            Text(viewModel.displayName(for: language))<br>                            Text(language.minimalIdentifier)<br>                        }<br>                        .tag(language)<br>                    }<br>                }<br>                .disabled(viewModel.filteredSourceLanguages.isEmpty)<br>                .onChange(of: viewModel.sourceLanguage) { _, _ in<br>                    Task {<br>                        await viewModel.checkLanguageSupport()<br>                    }<br>                }<br>            }<br>            <br>            // Target Language Section<br>            Section("Target Language") {<br>                TextField("Filter languages", text: $viewModel.targetFilter)<br>                <br>                Picker("Select Target Language", selection: $viewModel.targetLanguage) {<br>                    ForEach(viewModel.filteredTargetLanguages, id: \.maximalIdentifier) { language in<br>                        Button {} label: {<br>                            Text(viewModel.displayName(for: language))<br>                            Text(language.minimalIdentifier)<br>                        }<br>                        .tag(language)<br>                    }<br>                }<br>                .disabled(viewModel.filteredTargetLanguages.isEmpty)<br>                .onChange(of: viewModel.targetLanguage) { _, _ in<br>                    Task {<br>                        await viewModel.checkLanguageSupport()<br>                    }<br>                }<br>            }<br>            <br>            // Status Section<br>            Section {<br>                if viewModel.languageStatus == .installed {<br>                    Text("✅ Translation Installed")<br>                        .foregroundColor(.green)<br>                } else if viewModel.languageStatus == .supported {<br>                    Text("⬇️ Translation Available to Download")<br>                        .foregroundColor(.orange)<br>                } else {<br>                    Text("❌ Translation Not Supported")<br>                        .foregroundColor(.red)<br>                }<br>            }<br>            <br>            // Download Button Section<br>            if viewModel.languageStatus == .supported {<br>                NavigationLink("Download") {<br>                    TranslationModelDownloader(sourceLanguage: viewModel.sourceLanguage,<br>                                               targetLanguage: viewModel.targetLanguage)<br>                }<br>            }<br>        }<br>        .navigationTitle("Language Selector")<br>        .onAppear {<br>            Task {<br>                await viewModel.checkLanguageSupport()<br>            }<br>        }<br>    }<br>}<br><br>#Preview {<br>    LanguageAvailabilityChecker()<br>}<br><br>struct TranslationModelDownloader: View {<br>    <br>    var configuration: TranslationSession.Configuration<br>    <br>    init(sourceLanguage: Locale.Language, targetLanguage: Locale.Language) {<br>        self.configuration = TranslationSession.Configuration(source: sourceLanguage, target: targetLanguage)<br>    }<br>    <br>    var body: some View {<br>        NavigationView {<br>            Text("Download translation files between \(configuration.source?.minimalIdentifier ?? "?") and \(configuration.target?.minimalIdentifier ?? "?")")<br>            .translationTask(configuration) { session in<br>                do {<br>                    try await session.prepareTranslation()<br>                } catch {<br>                    // Handle any errors.<br>                    print("Error downloading translation: \(error)")<br>                }<br>            }<br>        }<br>    }<br>}</code></pre>



<h1 class="wp-block-heading" id="5eab">Get translated result programmatically</h1>



<p id="70e9">If you want to show the translated result within your app’s view. You can use a translation session.</p>



<p id="18e5">To get the translation result programmatically, you still need a SwiftUI shown on screen.</p>



<p id="f0d6">First, set up the variables to store the text to translate, an optional translation configuration, and one to store the result:</p>



<pre class="wp-block-code"><code>@State private var textToTranslate: String?<br>@State private var translationConfiguration: TranslationSession.Configuration?<br>@State private var translationResult: String?</code></pre>



<p id="91b2">Then, add a&nbsp;<code>translationTask</code>&nbsp;view modifier:</p>



<pre class="wp-block-code"><code>.translationTask(translationConfiguration) { session in<br>    do {<br>        guard let textToTranslate else { return }<br>        let response = try await session.translate(textToTranslate)<br>        self.translationResult = response.targetText<br>    } catch {<br>        print("Error: \(error)")<br>    }<br>}</code></pre>



<p id="0ee4">Now, when you are ready to translate (for example, you have fetched the text for translation, you can set a value to the variables). For example, in the below code, we fetch the content of a web page and translate it to Japanese.</p>



<pre class="wp-block-code"><code>let (data, _) = try await URLSession.shared.data(from: URL(string: "https://raw.githubusercontent.com/swiftlang/swift/refs/heads/main/.github/ISSUE_TEMPLATE/task.yml")!)<br>guard let webPageContent = String(data: data, encoding: .utf8) else { return }<br>// start a translation to Japanese<br>self.textToTranslate = webPageContent<br>self.translationConfiguration = .init(target: .init(identifier: "ja"))</code></pre>



<p id="9078">Notice that the&nbsp;<code>self.translationConfiguration</code>&nbsp;can take no parameters, just the source language code, just the target language code, or both. For the parameters that you do not provide, the system will infer automatically based on user configuration.</p>



<p id="b5de">It is recommended that you define the source and target language by yourself.</p>



<p id="8088">Here is the complete code:</p>



<pre class="wp-block-code"><code>import SwiftUI<br>import Translation<br><br>struct CustomTranslation: View {<br>    <br>    @State private var textToTranslate: String?<br>    @State private var translationConfiguration: TranslationSession.Configuration?<br>    @State private var translationResult: String?<br>    <br>    var body: some View {<br>        Form {<br>            <br>            Section("Original text") {<br>                if let textToTranslate {<br>                    Text(textToTranslate)<br>                }<br>            }<br>            <br>            Section("Translated text") {<br>                if let translationResult {<br>                    Text(translationResult)<br>                }<br>            }<br>            <br>        }<br>        .translationTask(translationConfiguration) { session in<br>            do {<br>                guard let textToTranslate else { return }<br>                let response = try await session.translate(textToTranslate)<br>                self.translationResult = response.targetText<br>            } catch {<br>                print("Error: \(error)")<br>            }<br>        }<br>        .task {<br>            // fetch the text<br>            do {<br>                let (data, response) = try await URLSession.shared.data(from: URL(string: "https://raw.githubusercontent.com/swiftlang/swift/refs/heads/main/.github/ISSUE_TEMPLATE/task.yml")!)<br>                guard let webPageContent = String(data: data, encoding: .utf8) else { return }<br>                // start a translation to Japanese<br>                self.textToTranslate = webPageContent<br>                self.translationConfiguration = .init(target: .init(identifier: "ja"))<br>            } catch {<br>                print("Error: \(error)")<br>            }<br>        }<br>    }<br>    <br>}<br><br>#Preview {<br>    CustomTranslation()<br>}</code></pre>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="400" height="869" src="https://static-assets.mszpro.com/2024/12/1Nu0wxFHoDvN82cva_LWb6w.gif" alt="" class="wp-image-489"/></figure>



<h1 class="wp-block-heading" id="05fb">Compatibility for older iOS versions</h1>



<p id="dfd9">The&nbsp;<code>translationTask</code>&nbsp;view modifier is only available for iOS 18 and higher. If you also want your app to support iOS 17, you can create a custom SwiftUI view modifier (that runs the translation task if it is iOS 18 or higher, and do nothing when it is not supported):</p>



<pre class="wp-block-code"><code>@ViewBuilder<br>public func translationTaskCompatible(<br>    shouldRun: Bool,<br>    textToTranslate: String,<br>    targetLanguage: Locale.Language = Locale.current.language,<br>    action: @escaping (_ detectedSourceLanguage: String, _ translationResult: String) -&gt; Void<br>) -&gt; some View {<br>    if shouldRun, #available(iOS 18.0, *) {<br>        self<br>            .translationTask(.init(target: targetLanguage), action: { session in<br>                do {<br>                    let response = try await session.translate(textToTranslate)<br>                    action(response.sourceLanguage.minimalIdentifier, response.targetText)<br>                } catch {<br>                    print("Translation failed: \(error.localizedDescription)")<br>                }<br>            })<br>    } else {<br>        self // No-op for unsupported iOS versions<br>    }<br>}</code></pre>



<p id="9282">Then, in your iOS code, you can use it like this:</p>



<pre class="wp-block-code"><code>.translationTaskCompatible(shouldRun: self.runAppleTranslation,<br>                               textToTranslate: self.displayedPostContent,<br>                               targetLanguage: Locale.current.language, action: { detectedSourceLanguageCode, translationResult in<br>        self.displayedPostContent = translationResult<br>        })</code></pre>



<h1 class="wp-block-heading" id="819d">Submit multiple translation requests</h1>



<p id="8a54">You can submit multiple translation requests, and have results coming with unique IDs whenever it becomes available:</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="600" height="1303" src="https://static-assets.mszpro.com/2024/12/1ysU4lvZj0034HAvz0fk18A.gif" alt="" class="wp-image-493"/></figure>



<p id="ed95">Within a Task block and a SwiftUI view, you can create an array of translation requests, and submit them all in once. Within each request, you can specify a request ID, so you know which original text it is referring to when you get the result back.</p>



<pre class="wp-block-code"><code>//<br>//  MultipleTranslate.swift<br>//  iOSTranslationVideo<br>//<br>//  Created by msz on 2024/12/05.<br>//<br><br>import SwiftUI<br>import Translation<br><br>struct MultipleTranslate: View {<br>    <br>    // translation struct with the original text and optional translated text String<br>    struct TranslationEntry: Identifiable {<br>        let id: String<br>        let originalText: String<br>        var translatedText: String?<br>        <br>        init(id: String = UUID().uuidString, originalText: String, translatedText: String? = nil) {<br>            self.id = id<br>            self.originalText = originalText<br>            self.translatedText = translatedText<br>        }<br>    }<br>    <br>    @State private var textsToTranslate: &#91;TranslationEntry] = &#91;<br>        .init(originalText: "Hello world! This is just a test."),<br>        .init(originalText: "The quick brown fox jumps over the lazy dog."),<br>        .init(originalText: "How are you doing today?"),<br>        .init(originalText: "It is darkest just before the dawn."),<br>        .init(originalText: "The early bird catches the worm."),<br>    ]<br>    @State private var userEnteredNewText: String = ""<br>    <br>    @State private var configuration: TranslationSession.Configuration?<br>    <br>    var body: some View {<br>        <br>        Form {<br>            <br>            // list all text<br>            Section("Texts to translate") {<br>                List {<br>                    ForEach(textsToTranslate) { text in<br>                        VStack(alignment: .leading) {<br>                            // original text<br>                            Text(text.originalText)<br>                                .font(.headline)<br>                            // translated text, if available<br>                            if let translatedText = text.translatedText {<br>                                Text(translatedText)<br>                                    .font(.subheadline)<br>                            }<br>                        }<br>                    }<br>                }<br>            }<br>            <br>            // allow user to add a new text, using a TextField and a Button<br>            Section("Add new text") {<br>                HStack {<br>                    TextField("Enter text to translate",<br>                              text: $userEnteredNewText)<br>                    Button("Add") {<br>                        textsToTranslate.append(.init(originalText: userEnteredNewText))<br>                        userEnteredNewText = ""<br>                    }<br>                }<br>            }<br>            <br>            Button("Translate all to Japanese") {<br>                self.configuration = .init(target: .init(identifier: "ja"))<br>            }<br>            <br>        }<br>        .translationTask(configuration) { session in<br>            let allRequests = textsToTranslate.map {<br>                return TranslationSession.Request(<br>                    sourceText: $0.originalText,<br>                    clientIdentifier: $0.id)<br>            }<br>            do {<br>                for try await response in session.translate(batch: allRequests) {<br>                    print(response.targetText, response.clientIdentifier ?? "")<br>                    if let i = self.textsToTranslate.firstIndex(where: { $0.id == response.clientIdentifier }) {<br>                        var entry = self.textsToTranslate&#91;i]<br>                        entry.translatedText = response.targetText<br>                        self.textsToTranslate.remove(at: i)<br>                        self.textsToTranslate.insert(entry, at: i)<br>                    }<br>                }<br>            } catch {<br>                print(error.localizedDescription)<br>            }<br>        }<br>        <br>    }<br>    <br>}<br><br>#Preview {<br>    MultipleTranslate()<br>}</code></pre>



<h1 class="wp-block-heading" id="1c02">Using in UIKit</h1>



<p id="b5ab">You might notice that all the above need a view modifier attached to a SwiftUI View.</p>



<p id="0808">Here is a quote from Apple Engineer:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p id="a3ec"><em>“While the Translation APIs do need to be triggered from SwiftUI, there’s still a straightforward workaround to get this working from a mostly UIKit (or AppKit) app. You can add&nbsp;</em><code><em>UIHostingController</em></code><em>&nbsp;(or&nbsp;</em><code><em>NSHostingController</em></code><em>) to the place in your app you want any translation UI to present from. You can add the&nbsp;</em><code><em>.translationPresentation</em></code><em>&nbsp;or&nbsp;</em><code><em>.translationTask</em></code><em>&nbsp;modifier to a simple SwiftUI view, even though most of your app doesn&#8217;t use SwiftUI.”&nbsp;</em><a href="https://developer.apple.com/forums/thread/756837?answerId=791116022#791116022" rel="noreferrer noopener" target="_blank">https://developer.apple.com/forums/thread/756837?answerId=791116022#791116022</a></p>
</blockquote>



<p id="29b1">So the translation API needs to be triggered from a SwiftUI view.</p>



<p id="8045">If you are using UIKit, you can use a&nbsp;<code><em>UIHostingController</em></code></p>



<pre class="wp-block-code"><code>//<br>//  TranslationUIKit.swift<br>//  iOSTranslationVideo<br>//<br>//  Created by msz on 2024/12/05.<br>//<br><br>import Foundation<br>import UIKit<br>import SwiftUI<br><br>#if canImport(Translation)<br>import Translation<br>#endif<br><br>struct EmbeddedTranslationView: View {<br>    var sourceText: String<br>    @State private var isTranslationShown: Bool = false<br>    <br>    var body: some View {<br>        VStack {<br>#if canImport(Translation)<br>            Button("Translate") {<br>                self.isTranslationShown = true<br>            }<br>            .translationPresentation(isPresented: $isTranslationShown,<br>                                     text: self.sourceText)<br>#else<br>            Text("Translation feature not available.")<br>#endif<br>        }<br>    }<br>}<br><br>// UIKit ViewController<br>class ViewController: UIViewController {<br>    override func viewDidLoad() {<br>        super.viewDidLoad()<br>        view.backgroundColor = .systemBackground<br>        <br>        // Create the SwiftUI view<br>        let embeddedSwiftUIView = EmbeddedTranslationView(sourceText: "Hello world! This is a test.")<br>        <br>        // Embed the SwiftUI view in a UIHostingController<br>        let hostingController = UIHostingController(rootView: embeddedSwiftUIView)<br>        <br>        // Add the UIHostingController as a child view controller<br>        addChild(hostingController)<br>        hostingController.view.translatesAutoresizingMaskIntoConstraints = false<br>        view.addSubview(hostingController.view)<br>        hostingController.didMove(toParent: self)<br>        <br>        // Layout the SwiftUI view<br>        NSLayoutConstraint.activate(&#91;<br>            hostingController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),<br>            hostingController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor),<br>            hostingController.view.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),<br>            hostingController.view.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5)<br>        ])<br>    }<br>}<br><br>// Wrap UIKit ViewController for SwiftUI<br>struct UIKitViewWrapper: UIViewControllerRepresentable {<br>    func makeUIViewController(context: Context) -&gt; UIViewController {<br>        return ViewController()<br>    }<br>    <br>    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {<br>        // No updates required for now<br>    }<br>}<br><br>// Add a SwiftUI Preview for Testing<br>struct UIKitViewWrapper_Previews: PreviewProvider {<br>    static var previews: some View {<br>        UIKitViewWrapper()<br>            .edgesIgnoringSafeArea(.all)<br>    }<br>}</code></pre>



<p id="66ef">In the above view, you will embed the SwiftUI view (which shows a single translate button). When the user taps that button, you can present the translation view.</p>



<p id="c7cc">Similarly, if you want to call the translation API, you can pass the translated value back from SwiftUI view to your UIKit view.</p>



<h1 class="wp-block-heading" id="9c09">Thank you for reading!</h1>



<p id="a8c1">The code in this article is available at:&nbsp;<a href="https://github.com/mszpro/iOS-System-Translation-Demo" rel="noreferrer noopener" target="_blank">https://github.com/mszpro/iOS-System-Translation-Demo</a></p>



<p id="2fc4">Japanese version:&nbsp;<a href="https://qiita.com/mashunzhe/items/d90ae92e7daba800abaf" rel="noreferrer noopener" target="_blank">https://qiita.com/mashunzhe/items/d90ae92e7daba800abaf</a></p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="586" height="488" src="https://static-assets.mszpro.com/2024/12/1PGkriQSbIQaMy_eBMW58tg.png" alt="" class="wp-image-488" srcset="https://static-assets.mszpro.com/2024/12/1PGkriQSbIQaMy_eBMW58tg-300x250.png 300w, https://static-assets.mszpro.com/2024/12/1PGkriQSbIQaMy_eBMW58tg.png 586w" sizes="auto, (max-width: 586px) 100vw, 586px" /></figure>



<p id="844b">Follow me on Twitter:&nbsp;<a href="https://twitter.com/mszpro" rel="noreferrer noopener" target="_blank">https://twitter.com/mszpro</a></p>



<p id="645e">Subscribe on Youtube:&nbsp;<a href="https://www.youtube.com/@MszPro6" rel="noreferrer noopener" target="_blank">https://www.youtube.com/@MszPro6</a></p>



<p id="8b88">Mastodon, Misskey: @me@mszpro.com</p>



<p id="98bd">Bluesky: @mszpro.com</p>



<p id="d083">Website:&nbsp;<a href="https://mszpro.com/" rel="noreferrer noopener" target="_blank">https://mszpro.com</a></p>



<p id="1a18">SoraSNS for Mastodon, Misskey, Bluesky, Nostr all in one:&nbsp;<a href="https://mszpro.com/sorasns" rel="noreferrer noopener" target="_blank">https://mszpro.com/sorasns</a></p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1080" height="567" src="https://static-assets.mszpro.com/2024/12/1UKKVmi2cx0vo6bLuz3RWQA.png" alt="" class="wp-image-487" srcset="https://static-assets.mszpro.com/2024/12/1UKKVmi2cx0vo6bLuz3RWQA-300x158.png 300w, https://static-assets.mszpro.com/2024/12/1UKKVmi2cx0vo6bLuz3RWQA-1024x538.png 1024w, https://static-assets.mszpro.com/2024/12/1UKKVmi2cx0vo6bLuz3RWQA-768x403.png 768w, https://static-assets.mszpro.com/2024/12/1UKKVmi2cx0vo6bLuz3RWQA.png 1080w" sizes="auto, (max-width: 1080px) 100vw, 1080px" /></figure><p>The post <a href="https://mszpro.com/ios-system-translate">Free translation API — using the iOS 18’s system Translation framework in your app (SwiftUI or UIKit)</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>SwiftUI .tabItem deprecated in iOS 18.1 -&gt; replace it with new Tab</title>
		<link>https://mszpro.com/swiftui-tabitem-deprecated</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Mon, 16 Dec 2024 08:36:02 +0000</pubDate>
				<category><![CDATA[API Deprecation]]></category>
		<category><![CDATA[Short Read]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=467</guid>

					<description><![CDATA[<p>In the newest Xcode, you might see a deprecation warning if you try to use .tabItem view modifier. 💡 &#8216;tabItem&#8217; will be deprecated in a future version of iOS: Use Tab (title:image:value:content:) and related initializers instead As shown on the Apple Developer documentation: You can easily fix this by using the new Tab view: You [&#8230;]</p>
<p>The post <a href="https://mszpro.com/swiftui-tabitem-deprecated">SwiftUI .tabItem deprecated in iOS 18.1 -> replace it with new Tab</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></description>
										<content:encoded><![CDATA[<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="521" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.webp" alt="" class="wp-image-468" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10-300x153.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10-768x391.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10-1536x782.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.webp 1984w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>In the newest Xcode, you might see a deprecation warning if you try to use <code>.tabItem</code> view modifier.</p>



<p>💡</p>



<p>&#8216;tabItem&#8217; will be deprecated in a future version of iOS: Use Tab (title:image:value:content:) and related initializers instead</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="367" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.52.53-AM.webp" alt="" class="wp-image-469" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.52.53-AM-300x107.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.52.53-AM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.52.53-AM-768x275.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.52.53-AM-1536x550.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.52.53-AM.webp 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>As shown on the Apple Developer documentation:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="521" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.53.19-AM.webp" alt="" class="wp-image-470" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.53.19-AM-300x153.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.53.19-AM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.53.19-AM-768x391.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.53.19-AM-1536x782.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.53.19-AM.webp 1984w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>You can easily fix this by using the new <code>Tab</code> view:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="381" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.55.52-AM.webp" alt="" class="wp-image-471" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.55.52-AM-300x112.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.55.52-AM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.55.52-AM-768x286.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.55.52-AM-1536x572.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.55.52-AM.webp 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>You can also easily add a badge (that shows a number or a character text), and use the <code>value</code> parameter to control which tab is currently activated:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="796" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.57.27-AM.webp" alt="" class="wp-image-472" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.57.27-AM-300x233.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.57.27-AM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.57.27-AM-768x597.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.57.27-AM-1536x1193.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-10-09-at-10.57.27-AM.webp 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure><p>The post <a href="https://mszpro.com/swiftui-tabitem-deprecated">SwiftUI .tabItem deprecated in iOS 18.1 -> replace it with new Tab</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>All things about SwiftUI Text: underline, strikethrough, kerning, inline SF symbol, combine multiple</title>
		<link>https://mszpro.com/swiftui-text</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Mon, 16 Dec 2024 08:14:32 +0000</pubDate>
				<category><![CDATA[SwiftUI]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=418</guid>

					<description><![CDATA[<p>Can you make the same view as shown in the header thumbnail? If you can, save your time by skipping this article. Otherwise, let&#8217;s go! Different default font size We will start off with the basics like font size. You can use the .font(.title3) to set a preset font size: If you want to use [&#8230;]</p>
<p>The post <a href="https://mszpro.com/swiftui-text">All things about SwiftUI Text: underline, strikethrough, kerning, inline SF symbol, combine multiple</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></description>
										<content:encoded><![CDATA[<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="678" height="382" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.webp" alt="" class="wp-image-419" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5-300x169.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.webp 678w" sizes="auto, (max-width: 678px) 100vw, 678px" /></figure>



<h3 class="wp-block-heading">Can you make the same view as shown in the header thumbnail?</h3>



<p>If you can, save your time by skipping this article. Otherwise, let&#8217;s go!</p>



<h3 class="wp-block-heading">Different default font size</h3>



<p>We will start off with the basics like font size.</p>



<p>You can use the <code>.font(.title3)</code> to set a preset font size:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="386" height="530" src="https://static-assets.mszpro.com/2024/12/1-_EXKaFPrSmz7RYevfEeB1w.webp" alt="" class="wp-image-420" srcset="https://static-assets.mszpro.com/2024/12/1-_EXKaFPrSmz7RYevfEeB1w-218x300.webp 218w, https://static-assets.mszpro.com/2024/12/1-_EXKaFPrSmz7RYevfEeB1w.webp 386w" sizes="auto, (max-width: 386px) 100vw, 386px" /></figure>



<p>If you want to use system style but a custom size, you can use <code>.font(.system(size: 60))</code></p>



<p>For using a custom font file (like .ttf) you can refer to here: <a href="https://developer.apple.com/documentation/swiftui/applying-custom-fonts-to-text/">https://developer.apple.com/documentation/swiftui/applying-custom-fonts-to-text/</a></p>



<h3 class="wp-block-heading">Underline</h3>



<p>You can add an underline with many styles like dashed, dotted.</p>



<p>If you do not provide any parameters for the <code>.underline()</code> view modifier, it will draw a solid line with the same color as text.</p>



<p>You can also supply a Bool variable to control whether the underline is active, specify a pattern (dash or dot or both), and specify a color.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="515" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.53.30-PM.webp" alt="" class="wp-image-421" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.53.30-PM-300x151.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.53.30-PM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.53.30-PM-768x386.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.53.30-PM-1536x772.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.53.30-PM.webp 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<pre class="wp-block-code"><code>struct TextModifiers: View {
    
    var body: some View {
        
        VStack {
            
            Text("I love SwiftUI")
                .font(.system(size: 60))
                .underline()
            
            Text("I love SwiftUI")
                .font(.system(size: 60))
                .underline(true, pattern: .dash, color: .teal)
            
            Text("I love SwiftUI")
                .font(.system(size: 60))
                .underline(true, pattern: .dot, color: .orange)
            
        }
        
    }
    
}</code></pre>



<h3 class="wp-block-heading">Strikethrough</h3>



<p>You can set strike through to use a line to cross the text.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="208" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.05.27-PM.webp" alt="" class="wp-image-422" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.05.27-PM-300x61.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.05.27-PM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.05.27-PM-768x156.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.05.27-PM-1536x312.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.05.27-PM.webp 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>Similar to the above <code>underline</code> view modifier, you can provide no parameters, or the parameter to control whether to activate the strikethrough, the pattern, or the color.</p>



<h3 class="wp-block-heading">Kerning</h3>



<p>This view modifier controls the spacing between each character.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="284" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5-1.webp" alt="" class="wp-image-423" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5-1-300x83.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5-1.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5-1-768x213.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5-1-1536x426.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5-1.webp 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">Combining multiple text with different styles</h3>



<p>You can join <code>Text</code> views by using the <code>+</code> operator.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="224" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5-2.webp" alt="" class="wp-image-424" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5-2-300x66.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5-2.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5-2-768x168.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5-2-1536x336.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5-2.webp 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">Adjust the offset (on vertical axis)</h3>



<p>You can use the <code>baselineOffset</code> view modifier to adjust the vertical offset of the text. You can also use the above method to create an artistic looking text:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="394" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.18.37-PM.webp" alt="" class="wp-image-425" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.18.37-PM-300x116.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.18.37-PM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.18.37-PM-768x296.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.18.37-PM-1536x591.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.18.37-PM.webp 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">Control where text is aligned for multiple lines</h3>



<p>You can use the <code>multilineTextAlignment</code> to control multi-line text alignment, whether all aligned to the left (leading) edge, the center, or the right (trailing edge)</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="398" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.23.29-PM.webp" alt="" class="wp-image-426" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.23.29-PM-300x117.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.23.29-PM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.23.29-PM-768x298.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.23.29-PM-1536x597.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.23.29-PM.webp 1606w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="398" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.22.55-PM.webp" alt="" class="wp-image-427" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.22.55-PM-300x117.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.22.55-PM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.22.55-PM-768x299.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.22.55-PM-1536x598.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.22.55-PM.webp 1640w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="409" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.23.40-PM.webp" alt="" class="wp-image-428" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.23.40-PM-300x120.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.23.40-PM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.23.40-PM-768x306.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.23.40-PM-1536x613.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.23.40-PM.webp 1624w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">Font design</h3>



<p>You can change the font design to fit the looking of your overall view:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="387" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.27.18-PM.webp" alt="" class="wp-image-429" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.27.18-PM-300x113.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.27.18-PM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.27.18-PM-768x290.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.27.18-PM-1536x581.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.27.18-PM.webp 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>For example, you can use <code>monospaced</code> design to help the user see each character of the text clearly (for example, when displaying a username or password).</p>



<h3 class="wp-block-heading">Show SF Symbol within text</h3>



<p>You can show system symbol in line with the text:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="604" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.30.00-PM.webp" alt="" class="wp-image-430" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.30.00-PM-300x177.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.30.00-PM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.30.00-PM-768x453.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.30.00-PM-1536x906.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-5.30.00-PM.webp 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>You can also use the <code>\()</code> format to show a variable like a number.</p>



<h3 class="wp-block-heading">Complete example code:</h3>



<pre class="wp-block-code"><code>//
//  TextModifiers.swift
//  WhatsNewIniOS18
//
//  Created by Msz on 8/10/24.
//

import SwiftUI

struct TextModifiers: View {
    
    var body: some View {
        
        List {
            
            Text("I love SwiftUI")
                .font(.system(size: 60))
                .underline()
            
            Text("I love SwiftUI")
                .font(.system(size: 60))
                .underline(true, pattern: .dash, color: .teal)
            
            Text("I love SwiftUI")
                .font(.system(size: 60))
                .underline(true, pattern: .dot, color: .orange)
            
            Text("UIStoryboard")
                .font(.system(size: 60))
                .strikethrough()
            
            Text("UIStoryboard")
                .font(.system(size: 60))
                .strikethrough(true, pattern: .dash, color: .orange)
            
            Text("SwiftUI")
                .font(.system(size: 60))
                .kerning(1)
            
            Text("SwiftUI")
                .font(.system(size: 60))
                .kerning(6)
            
            Text("SwiftUI")
                .font(.system(size: 60))
                .kerning(12)
            
            Text("I love ").font(.largeTitle).bold() + Text("SwiftUI").font(.largeTitle).foregroundStyle(.orange).bold()
            
            Text("\(Image(systemName: "swift")) Text")
                .font(.title)
                .bold()
                .baselineOffset(20)
                .strikethrough(pattern: .dot)
            + Text("完全に")
                .font(.largeTitle)
                .bold()
                .foregroundStyle(.pink)
                .underline()
            + Text("理解した \(Image(systemName: "checkmark"))")
                .font(.largeTitle)
                .foregroundStyle(.teal)
                .bold()
                .baselineOffset(20)
                .fontDesign(.serif)
            
            Text("This is a super long text that will eventually wrap around the screen okay now how do we control how the text is aligned? left, center, or right. Wait! It is called leading and trailing.")
                .multilineTextAlignment(.trailing)
            
            Text("SwiftUI")
                .font(.largeTitle)
                .monospaced() //.fontDesign(.monospaced)
            
            Text("SwiftUI")
                .font(.largeTitle)
                .fontDesign(.rounded)
            
            Text("SwiftUI")
                .font(.largeTitle)
                .fontDesign(.serif)
            
        }
        
    }
    
}

#Preview {
    TextModifiers()
}
</code></pre><p>The post <a href="https://mszpro.com/swiftui-text">All things about SwiftUI Text: underline, strikethrough, kerning, inline SF symbol, combine multiple</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>SwiftUI List control spacing between sections (listSectionSpacing)</title>
		<link>https://mszpro.com/swiftui-list-section-spacing</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Mon, 16 Dec 2024 08:11:03 +0000</pubDate>
				<category><![CDATA[Short Read]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=413</guid>

					<description><![CDATA[<p>In SwiftUI List, you can use the listSectionSpacing view modifier for List view to control how much space is in between different sections. Base code We will use this code as a starting point: Default spacing Here, you can set the default spacing using the .default as the input to the view modifier. Compact spacing [&#8230;]</p>
<p>The post <a href="https://mszpro.com/swiftui-list-section-spacing">SwiftUI List control spacing between sections (listSectionSpacing)</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>In SwiftUI List, you can use the <code>listSectionSpacing</code> view modifier for <code>List</code> view to control how much space is in between different sections.</p>



<h3 class="wp-block-heading">Base code</h3>



<p>We will use this code as a starting point:</p>



<pre class="wp-block-code"><code>//
//  ListSpacingControl.swift
//  WhatsNewIniOS18
//
//  Created by Msz on 8/10/24.
//

import SwiftUI

struct ListSpacingControl: View {
    
    let planets: &#91;String] = &#91;"Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
    let closeGalaxies = &#91;
        "Milky Way",
        "Andromeda Galaxy",
        "Large Magellanic Cloud",
        "Small Magellanic Cloud",
        "Triangulum Galaxy"
    ]
    
    var body: some View {
        
        List {
            
            Section("Planets") {
                ForEach(planets, id: \.self) { planet in
                    Text(planet)
                }
            }
            
            Section("Galaxies") {
                ForEach(closeGalaxies, id: \.self) { planet in
                    Text(planet)
                }
            }
            
        }
        .listSectionSpacing(.default)
        
    }
    
}

#Preview {
    ListSpacingControl()
}
</code></pre>



<h3 class="wp-block-heading">Default spacing</h3>



<p>Here, you can set the default spacing using the <code>.default</code> as the input to the view modifier.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="741" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.36.49-PM.webp" alt="" class="wp-image-414" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.36.49-PM-300x217.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.36.49-PM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.36.49-PM-768x556.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.36.49-PM-1536x1112.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.36.49-PM.webp 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">Compact spacing</h3>



<p>You can set it to <code>compact</code> for relatively small spacing between different sections.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="632" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.38.42-PM.webp" alt="" class="wp-image-415" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.38.42-PM-300x185.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.38.42-PM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.38.42-PM-768x474.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.38.42-PM-1536x948.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.38.42-PM.webp 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">Custom spacing</h3>



<p>You can set a custom spacing by providing a number.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="579" src="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.39.20-PM.webp" alt="" class="wp-image-416" srcset="https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.39.20-PM-300x170.webp 300w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.39.20-PM.webp 1024w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.39.20-PM-768x434.webp 768w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.39.20-PM-1536x869.webp 1536w, https://static-assets.mszpro.com/2024/12/Screenshot-2024-08-10-at-4.39.20-PM.webp 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p></p><p>The post <a href="https://mszpro.com/swiftui-list-section-spacing">SwiftUI List control spacing between sections (listSectionSpacing)</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Making a user-configurable widget for your iOS app</title>
		<link>https://mszpro.com/configurable-widget</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Mon, 16 Dec 2024 07:52:07 +0000</pubDate>
				<category><![CDATA[iOS Home Screen]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=389</guid>

					<description><![CDATA[<p>In this article, we&#8217;ll cover the following: You can check out the completed source code here. Adding Widgets to an Existing Application Creating the Application Target Adding a widget to an existing iOS application is simple. Add the target Widget Extension. Now, make sure you have checked the Include Configuration Intent box. We&#8217;ll need that [&#8230;]</p>
<p>The post <a href="https://mszpro.com/configurable-widget">Making a user-configurable widget for your iOS app</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></description>
										<content:encoded><![CDATA[<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="320" height="693" src="https://static-assets.mszpro.com/2024/12/https-qiita-image-store.s3.ap-northeast-1.amazonaws.com-0-635330-1b643c98-60e5-3195-d0b1-e03a027de0c0.gif" alt="" class="wp-image-390"/></figure>



<p><strong>In this article, we&#8217;ll cover the following:</strong></p>



<ol class="wp-block-list">
<li>Adding Widgets to an Existing Application</li>



<li>⭐️ Adding Configurable Widgets (e.g., user selects a city)</li>



<li>Reloading Widgets</li>
</ol>



<p><a href="https://github.com/mszmagic/Configurable-Widget_Example">You can check out the completed source code here.</a></p>



<h2 class="wp-block-heading">Adding Widgets to an Existing Application</h2>



<h3 class="wp-block-heading">Creating the Application Target</h3>



<p>Adding a widget to an existing iOS application is simple. Add the target <code>Widget Extension</code>.</p>



<figure class="wp-block-image"><img decoding="async" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/d81c538d-13fa-05b4-0957-3c152bc8fb61.png" alt="image"/></figure>



<p>Now, make sure you have checked the <code>Include Configuration Intent</code> box. We&#8217;ll need that configuration file in part 2 of this article.</p>



<figure class="wp-block-image"><img decoding="async" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/280a43e9-a6e5-8657-2625-e54898bb9461.png" alt="image"/></figure>



<h3 class="wp-block-heading">Data Structure</h3>



<p>You should now see a new folder named <code>WidgetExample-Widget</code>. Click to open the file <code>WidgetExample_Widget.swift</code>, delete its contents, and follow this guide.</p>



<p>You can create the data structure for the data you want to display in your widget. In this example, we&#8217;ll display information about a cat!</p>



<pre class="wp-block-code"><code>struct CatEntry: TimelineEntry {
    var date: Date
    
    var name: String
    var lastFed: Date
    var lastPlayedWith: Date
}
</code></pre>



<h3 class="wp-block-heading">Creating the <code>IntentTimelineProvider</code> Structure</h3>



<p>The <code>IntentTimelineProvider</code> structure provides three types of content:</p>



<ol class="wp-block-list">
<li><code>placeholder</code> is displayed while the widget is loading.</li>



<li><code>getSnapshot</code> is shown in the widget gallery.</li>



<li><code>getTimeline</code> is used for the actual widget display.</li>
</ol>



<p>First, create a struct conforming to the <code>IntentTimelineProvider</code> type, then define the type of <code>Entry</code>.</p>



<pre class="wp-block-code"><code>struct CatProviderStatic: TimelineProvider {
    typealias Entry = CatEntry

    func getSnapshot(in context: Context, completion: @escaping (CatEntry) -&gt; Void) {
        //TODO
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline&lt;CatEntry&gt;) -&gt; Void) {
        //TODO
    }

    func placeholder(in context: Context) -&gt; CatEntry {
        //TODO
    }
}
</code></pre>



<p>For the <code>getSnapshot</code> function, you can provide example values for the widget view to help users understand what information your widget will provide:</p>



<pre class="wp-block-code"><code>func getSnapshot(in context: Context, completion: @escaping (CatEntry) -&gt; Void) {
    let entry = CatEntry(date: Date(), name: "Cat Name", lastFed: Date(), lastPlayedWith: Date())
    completion(entry)
}
</code></pre>



<p>For the placeholder view, you can display empty or example values:</p>



<pre class="wp-block-code"><code>func placeholder(in context: Context) -&gt; CatEntry {
    return CatEntry(date: Date(), name: "Cat Name", lastFed: Date(), lastPlayedWith: Date())
}
</code></pre>



<p>For the timeline display, you can provide the actual content to be displayed.</p>



<p>In this example, we display static data values. In your application, you can fetch content from <code>Core Data</code> (see <a href="https://qiita.com/MaShunzhe/items/6d13422aee5dcfaf2cc2">this article</a> on how to share data), online, from <code>CloudKit</code>, or from <code>UserDefaults</code>.</p>



<pre class="wp-block-code"><code>func getTimeline(in context: Context, completion: @escaping (Timeline&lt;CatEntry&gt;) -&gt; Void) {
    let entry = CatEntry(date: Date(), name: "Neko No Hī", lastFed: Date(), lastPlayedWith: Date())
    let timeline = Timeline(entries: &#91;entry], policy: .atEnd)
    completion(timeline)
}
</code></pre>



<h4 class="wp-block-heading">Adding Multiple Items to the Timeline</h4>



<p>You can also add multiple items to the timeline. The widget will automatically check the timeline items and reload at the times indicated.</p>



<pre class="wp-block-code"><code>func getTimeline(in context: Context, completion: @escaping (Timeline&lt;CatEntry&gt;) -&gt; Void) {
    var timelineEntries = &#91;CatEntry]()
    if let date1 = Calendar.current.date(byAdding: .hour, value: 1, to: Date()) {
        let entry = CatEntry(date: date1, name: "Neko No Hī", lastFed: date1, lastPlayedWith: date1)
        timelineEntries.append(entry)
    }
    if let date2 = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) {
        let entry = CatEntry(date: date2, name: "Neko No Hī", lastFed: date2, lastPlayedWith: date2)
        timelineEntries.append(entry)
    }
    let timeline = Timeline(entries: timelineEntries, policy: .atEnd)
    completion(timeline)
}
</code></pre>



<h3 class="wp-block-heading">Designing the Widget View</h3>



<p>Now, you can design the SwiftUI view for your widget.</p>



<pre class="wp-block-code"><code>struct CatWidgetView: View {
    
    @Environment(\.widgetFamily) var family
    
    var entry: CatEntry
    
    var body: some View {
        
        VStack {
            
            if family == .systemMedium || family == .systemLarge {
                Image("kitty")
                    .resizable()
                    .frame(width: 50, height: 50)
                    .padding(.vertical, 5)
            }
            
            Text(entry.name)
                .font(.headline)
                .padding(1)
            
            Text("Last played with at " + entry.lastPlayedWith.getString())
                .font(.caption)
                .padding(.horizontal)
            
            Text("Last fed at " + entry.lastFed.getString())
                .font(.caption)
                .padding(.horizontal)
            
        }
        
    }
}
</code></pre>



<p>You can use the <code>@Environment(\.widgetFamily) var family</code> variable to check the size of the widget.</p>



<p>In this example, we&#8217;re displaying a cat image if the widget is large enough to fit the image.</p>



<pre class="wp-block-code"><code>if family == .systemMedium || family == .systemLarge {
    Image("kitty")
        .resizable()
        .frame(width: 50, height: 50)
        .padding(.vertical, 5)
}
</code></pre>



<h3 class="wp-block-heading">Coding the Widget Application</h3>



<p>Now, you can code the widget application.</p>



<pre class="wp-block-code"><code>@main
struct CatWidget: Widget {
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: "CatWidget", intent: ConfigurationIntent.self, provider: CatProvider()) { entry in
            CatWidgetView(entry: entry)
        }.configurationDisplayName("Cat")
        .description("See when you last fed or played with your cat.")
    }
}
</code></pre>



<p>Now you can run the app on the simulator and add the widget you just designed to your screen.</p>



<figure class="wp-block-image"><img decoding="async" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/83eba8db-1e30-da2c-d9f0-2ed0cbeff687.jpeg" alt="image"/></figure>



<h2 class="wp-block-heading">Adding Configurable Widgets</h2>



<p>If you&#8217;ve used a weather widget, you&#8217;ll know that by long-pressing on the widget, users can configure it to display different cities. You can add this functionality using the <code>Intents</code> framework and Target.</p>



<h3 class="wp-block-heading">Adding the <code>Intents</code> Program Target</h3>



<figure class="wp-block-image"><img decoding="async" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/aab04398-6109-2708-1590-9dac82cec707.png" alt="image"/></figure>



<p>At this stage, UI elements are unnecessary, so uncheck the <code>Include UI Extension</code> option.</p>



<figure class="wp-block-image"><img decoding="async" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/6da8d52d-1fab-87bf-33e6-eb0289a70527.png" alt="image"/></figure>



<p>In the created Intents target page, find the section named <code>Supported Intents</code>. Create a new item named <code>ConfigurationIntent</code>. Now, you can name this to anything, but make sure to be consistent and use the same name for the upcoming steps.</p>



<figure class="wp-block-image"><img decoding="async" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/ceb58192-71bc-cc58-2747-850a82100373.png" alt="スクリーンショット 0002-10-09 15.16.55.png"/></figure>



<figure class="wp-block-image"><img decoding="async" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/c49dd939-7893-4d94-9070-82b15ba18f8c.png" alt="スクリーンショット 0002-10-09 15.11.01.png"/></figure>



<h3 class="wp-block-heading">Configuring <code>.intentdefinition</code></h3>



<p>Next, in</p>



<p>the previously created widget, add the newly created <code>Intents</code> extension as the intent target for the <code>WidgetExample_Widget.intentdefinition</code> file.</p>



<p>If you do not yet have an <code>intentdefinition</code> file, you can create one. Make sure to link the file to both the widget and the intent target.</p>



<figure class="wp-block-image"><img decoding="async" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/a9450674-1f3b-9d1d-f8c0-69221b74e7c2.png" alt="image"/></figure>



<p>Click <code>Configuration</code> on the left side of the screen. If no configurations exists, tap the plus mark and create a new intent, and modify the name to be <code>Configuration</code></p>



<figure class="wp-block-image"><img decoding="async" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/88b6539b-5891-275c-fb34-e4189715cb73.png" alt="image"/></figure>



<p>On the right side, ensure the configuration matches the following image.</p>



<figure class="wp-block-image"><img decoding="async" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/91f13c2f-f065-162f-19c4-cd9520f2a35d.png" alt="image"/></figure>



<p>Next, add a new parameter named <code>cat</code>.</p>



<figure class="wp-block-image"><img decoding="async" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/c6c7d415-0f7c-ad5d-3570-7eacb63b463a.png" alt="image"/></figure>



<p>In the settings screen for the newly created <code>cat</code> parameter, select <code>String</code> for the <code>Type</code> to use as the identifier for the cat.</p>



<figure class="wp-block-image"><img decoding="async" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/449616b7-e09d-f06c-9d1d-7386b92c24b4.png" alt="image"/></figure>



<h3 class="wp-block-heading">Configuring <code>IntentHandler</code></h3>



<p>Next, open the <code>IntentHandler.swift</code> file. This file provides options for users to configure the widget. In this example, the option will be the cat identifier.</p>



<p>Add the keyword <code>ConfigurationIntentHandling</code> next to the class type <code>INExtension</code> to allow Xcode to automatically display the next function names to add.</p>



<pre class="wp-block-code"><code>class IntentHandler: INExtension, ConfigurationIntentHandling {
    ...
}
</code></pre>



<p>In this example, the completed <code>IntentHandler.swift</code> file looks like this:</p>



<pre class="wp-block-code"><code>class IntentHandler: INExtension, ConfigurationIntentHandling {
    
    func provideCatOptionsCollection(for intent: ConfigurationIntent, searchTerm: String?, with completion: @escaping (INObjectCollection&lt;NSString&gt;?, Error?) -&gt; Void) {
        let catIdentifiers: &#91;NSString] = &#91;
            "Neko No Hī",
            "Mugi",
            "Azuki"
        ]
        let allCatIdentifiers = INObjectCollection(items: catIdentifiers)
        completion(allCatIdentifiers, nil)
    }
    
    override func handler(for intent: INIntent) -&gt; Any {
        // This is the default implementation. If you want different objects to handle different intents,
        // you can override this and return the handler you want for that particular intent.
        return self
    }
}
</code></pre>



<p>In the <code>provideCatOptionsCollection</code> function, you need to input a list of values. These values can actually be fetched from <code>User Defaults</code>, <code>Core Data</code>, or online. In this example, the values are hard-coded.</p>



<p><a href="https://qiita.com/MaShunzhe/items/6d13422aee5dcfaf2cc2">Using <code>Core Data</code> with App Extensions</a></p>



<h3 class="wp-block-heading">Creating <code>IntentTimelineProvider</code></h3>



<p>In part 1 of this article, we used <code>TimelineProvider</code>. This time, we&#8217;ll use <code>IntentTimelineProvider</code>.</p>



<p>If you already have a regular timeline provider, you should replace all the function headers (parameters).</p>



<p>The data structures between <code>IntentTimelineProvider</code> and <code>TimelineProvider</code> are almost identical. The difference is that you&#8217;ll need to declare an additional <code>typealias</code>.</p>



<pre class="wp-block-code"><code>typealias Intent = ConfigurationIntent
</code></pre>



<p>Another difference is that each function receives an additional parameter representing the intent selection <code>ConfigurationIntent</code>.</p>



<pre class="wp-block-code"><code>struct CatProvider: IntentTimelineProvider {
    
    typealias Intent = ConfigurationIntent
    typealias Entry = CatEntry
    
    func placeholder(in context: Context) -&gt; CatEntry {
        let entry = CatEntry(date: Date(), name: "", lastFed: Date(), lastPlayedWith: Date())
        return entry
    }
    
    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (CatEntry) -&gt; Void) {
        let entry = CatEntry(date: Date(), name: "Cat Name", lastFed: Date(), lastPlayedWith: Date())
        completion(entry)
    }
    
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline&lt;CatEntry&gt;) -&gt; Void) {
        let entry = CatEntry(date: Date(), name: configuration.cat ?? "", lastFed: Date(), lastPlayedWith: Date())
        let timeline = Timeline(entries: &#91;entry], policy: .atEnd)
        completion(timeline)
    }
}
</code></pre>



<p>You can use the <code>configuration.cat</code> property to read the value of the option selected by the user.</p>



<pre class="wp-block-code"><code>let entry = CatEntry(date: Date(), name: configuration.cat ?? "", lastFed: Date(), lastPlayedWith: Date())
</code></pre>



<h3 class="wp-block-heading">Updating <code>CatWidget</code> Code</h3>



<p>In part 1, we used <code>StaticConfiguration</code>. In this part, we&#8217;ll use <code>IntentConfiguration</code> (the name set in <code>Supported Intents</code>).</p>



<pre class="wp-block-code"><code>@main
struct CatWidget: Widget {
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: "CatWidget", intent: ConfigurationIntent.self, provider: CatProvider()) { entry in
            CatWidgetView(entry: entry)
        }.configurationDisplayName("Cat")
        .description("See when you last fed or played with your cat.")
    }
}
</code></pre>



<p>Now you can run the program on the simulator. By long-pressing the widget, you&#8217;ll see an option named <code>Edge Widget</code> and you can change the cat name.</p>



<figure class="wp-block-image"><img decoding="async" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/1b643c98-60e5-3195-d0b1-e03a027de0c0.gif" alt="ezgif-6-bf9e4b4783c9.gif"/></figure>



<h1 class="wp-block-heading">Reloading Widgets</h1>



<p>If content in the widget changes, you can manually call the reload function for the widget from the main iOS application.</p>



<pre class="wp-block-code"><code>// import WidgetKit
WidgetCenter.shared.reloadAllTimelines()
</code></pre>



<p>For example, if you have a ToDo app and a widget displaying the number of ToDo items, you can reload the widget when the user completes or adds a ToDo item.</p>



<p>:relaxed: <a href="https://twitter.com/MszPro">Twitter @MszPro</a></p>



<p>:sunny: <a href="https://mszpro.com/ioskiji/">Check out my list of publicly available Qiita articles by category.</a></p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="365" height="600" src="https://static-assets.mszpro.com/2024/12/AppClipImage_small.png" alt="" class="wp-image-391" srcset="https://static-assets.mszpro.com/2024/12/AppClipImage_small-183x300.png 183w, https://static-assets.mszpro.com/2024/12/AppClipImage_small.png 365w" sizes="auto, (max-width: 365px) 100vw, 365px" /></figure><p>The post <a href="https://mszpro.com/configurable-widget">Making a user-configurable widget for your iOS app</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>SwiftUIでTipKitを使用してユーザーにヒントを表示（iOS 17、WWDC 2023）</title>
		<link>https://mszpro.com/tipkit-ios17-ja</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Mon, 16 Dec 2024 07:28:13 +0000</pubDate>
				<category><![CDATA[SwiftUI]]></category>
		<category><![CDATA[TipKit]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=304</guid>

					<description><![CDATA[<p>TipKitフレームワークを使用して、アプリ内のさまざまな機能をユーザーに発見させる方法。インラインヒントやフローティングヒントの表示、条件やカウンターに基づくヒントの活用、さらにヒントのアクションボタンのカスタマイズについて説明します。</p>
<p>The post <a href="https://mszpro.com/tipkit-ios17-ja">SwiftUIでTipKitを使用してユーザーにヒントを表示（iOS 17、WWDC 2023）</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>この記事では、iOS 17の新しいTipKitフレームワークを使用して、アプリ内のさまざまな機能をユーザーに発見させる方法について説明します。</p>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F5c1f1923-ba2d-31e2-6a2c-a5ec76bd1494.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=9df042b23d03dddc92144fc456c70410" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="1290" height="682" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5c1f1923-ba2d-31e2-6a2c-a5ec76bd1494.jpg" alt="tipkit-inline-2.jpg" class="wp-image-307" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5c1f1923-ba2d-31e2-6a2c-a5ec76bd1494-300x159.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5c1f1923-ba2d-31e2-6a2c-a5ec76bd1494-1024x541.jpg 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5c1f1923-ba2d-31e2-6a2c-a5ec76bd1494-768x406.jpg 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5c1f1923-ba2d-31e2-6a2c-a5ec76bd1494.jpg 1290w" sizes="auto, (max-width: 1290px) 100vw, 1290px" /></a></figure>



<p>本記事の内容は以下の通りです：</p>



<ul class="wp-block-list">
<li>インラインヒントの表示</li>



<li>フローティングヒントの表示</li>



<li>条件に基づくヒントの表示</li>



<li>カウンターに基づくヒントの表示</li>



<li>ヒントのアクションボタンのカスタマイズ</li>
</ul>



<p>この記事は、主にSwiftUIを対象としています。</p>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7#tip%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E5%AE%9A%E7%BE%A9"></a>Tipオブジェクトの定義</h2>



<p>Tipオブジェクトには、画像、タイトル、説明、およびヒントのアクションが含まれています。以下はシンプルなヒントの例です：</p>



<p><code>var asset: Image?</code>&nbsp;には、ヒントの左側に表示するオプションの画像を提供できます。<br><code>var title: Text</code>&nbsp;には、このヒントのタイトルとして表示したいSwiftUIのテキストを提供します。<br><code>var message: Text?</code>&nbsp;には、タイトルの下部に表示されるオプションの説明文字列を提供できます。</p>



<pre class="wp-block-code"><code>import TipKit

struct HashTagPostButtonTip: Tip {
    var image: Image? {
        Image(systemName: "star.bubble")
    }
    var title: Text {
        Text("Send a Quick Response")
    }
    var message: Text? {
        Text("Double-tap a message, then choose a Tapback, like a ♥︎.")
    }
}
</code></pre>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2Fc30a3b45-5798-7d39-8da3-afc2e7a139b1.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=c520fd07d629588424135f6c14bd06c6" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="1290" height="790" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fc30a3b45-5798-7d39-8da3-afc2e7a139b1.jpg" alt="tipkit-floating.jpg" class="wp-image-312" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fc30a3b45-5798-7d39-8da3-afc2e7a139b1-300x184.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fc30a3b45-5798-7d39-8da3-afc2e7a139b1-1024x627.jpg 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fc30a3b45-5798-7d39-8da3-afc2e7a139b1-768x470.jpg 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fc30a3b45-5798-7d39-8da3-afc2e7a139b1.jpg 1290w" sizes="auto, (max-width: 1290px) 100vw, 1290px" /></a></figure>



<p>また、<code>var rules: [Rule]</code>&nbsp;および&nbsp;<code>var actions: [Action]</code>&nbsp;パラメータもありますが、これについては記事の次のセクションで説明します。</p>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7#%E3%83%92%E3%83%B3%E3%83%88%E3%81%AE%E8%A1%A8%E7%A4%BA"></a>ヒントの表示</h2>



<p>ヒントを表示する方法は2つあります。</p>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2Fef3cc4e1-e19b-bd34-1133-0c77c0c65da0.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=6f6ebb6e41fb99ba752cf38d6bdd008b" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="720" height="561" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fef3cc4e1-e19b-bd34-1133-0c77c0c65da0.jpg" alt="apple-tip-view-ways.jpeg" class="wp-image-311" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fef3cc4e1-e19b-bd34-1133-0c77c0c65da0-300x234.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fef3cc4e1-e19b-bd34-1133-0c77c0c65da0.jpg 720w" sizes="auto, (max-width: 720px) 100vw, 720px" /></a></figure>



<p>(from WWDC video)</p>



<h3 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7#%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3%E3%83%92%E3%83%B3%E3%83%88"></a>インラインヒント</h3>



<p>ヒントをビュー内に表示することができます。ヒントビューは、あなたの機能（例：ボタン）を指す矢印とともに表示されます。これは、TipViewをビューコードに直接含めることで実現できます：</p>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F5c1f1923-ba2d-31e2-6a2c-a5ec76bd1494.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=9df042b23d03dddc92144fc456c70410" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="1290" height="682" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5c1f1923-ba2d-31e2-6a2c-a5ec76bd1494.jpg" alt="tipkit-inline-2.jpg" class="wp-image-307" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5c1f1923-ba2d-31e2-6a2c-a5ec76bd1494-300x159.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5c1f1923-ba2d-31e2-6a2c-a5ec76bd1494-1024x541.jpg 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5c1f1923-ba2d-31e2-6a2c-a5ec76bd1494-768x406.jpg 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5c1f1923-ba2d-31e2-6a2c-a5ec76bd1494.jpg 1290w" sizes="auto, (max-width: 1290px) 100vw, 1290px" /></a></figure>



<pre class="wp-block-code"><code>import SwiftUI
import TipKit

struct TipWithArrow: View {
    var body: some View {
        VStack {
            
            HStack {
                TipView(HashTagPostButtonTip(), arrowEdge: .trailing)
                
                Image(systemName: "number")
                    .font(.system(size: 30))
                    .foregroundStyle(.white)
                    .padding()
                    .background { Circle().foregroundStyle(.blue) }
            }
            
        }
        .padding()
        .task {
            try? Tips.configure(&#91;
                .displayFrequency(.immediate),
                .datastoreLocation(.applicationDefault)
            ])
        }
    }
}

#Preview {
    TipWithArrow()
}
</code></pre>



<p><code>arrowEdge</code>&nbsp;パラメータを使用して、矢印の指す方向を決定することができます。<br><code>.trailing</code>&nbsp;に設定すると、ヒントは右側を指す矢印を表示します（つまり、機能ボタンが右側にあります）。<br>また、機能ボタンが左側にあり（表示されているヒントがボタンの右側にある場合）、<code>.leading</code>に設定します。</p>



<h3 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7#%E3%83%95%E3%83%AD%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%83%9D%E3%83%83%E3%83%97%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%92%E3%83%B3%E3%83%88"></a>フローティング（ポップオーバー）ヒント</h3>



<p>ボタンにフローティングヒントを添付するためのビューモディファイアも使用できます。これは、例えばナビゲーションボタンに対するヒントを表示したい場合に便利です：</p>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2Fd8555fc2-166b-6750-4c46-979233271321.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=e75e657a366bdce5c4268a4123d3690c" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="1290" height="740" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fd8555fc2-166b-6750-4c46-979233271321.jpg" alt="floating-tip.jpg" class="wp-image-309" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fd8555fc2-166b-6750-4c46-979233271321-300x172.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fd8555fc2-166b-6750-4c46-979233271321-1024x587.jpg 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fd8555fc2-166b-6750-4c46-979233271321-768x441.jpg 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fd8555fc2-166b-6750-4c46-979233271321.jpg 1290w" sizes="auto, (max-width: 1290px) 100vw, 1290px" /></a></figure>



<pre class="wp-block-code"><code>struct PopOverTip: View {
    
    var hashtagButtonTip = HashTagPostButtonTip()
    
    var body: some View {
        VStack {
            
            Text("Hello world!")
            
        }
        .padding()
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Image(systemName: "number")
                    .onTapGesture {
                        hashtagButtonTip.invalidate(reason: .actionPerformed)
                    }
                    .popoverTip(hashtagButtonTip)
            }
        }
        .task {
            try? Tips.configure(&#91;
                .displayFrequency(.immediate),
                .datastoreLocation(.applicationDefault)
            ])
        }
    }
    
}

#Preview {
    PopOverTip()
}

</code></pre>



<p>If you want your app to be compatible also with previous iOS versions, you can use a similar technique as shown below to create a conditional view modifier:</p>



<pre class="wp-block-code"><code>extension View {
	@ViewBuilder
	func tipIfAvailable(with tip: Tip) -&gt; some View {
		if #available(iOS 17, *) {
			self
				.popoverTip(tip)
		}
	}
}
</code></pre>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7#%E3%83%92%E3%83%B3%E3%83%88%E3%81%AE%E9%A0%BB%E5%BA%A6"></a>ヒントの頻度</h2>



<p>デフォルトでは、各ヒントは一度だけ表示されます。<br><code>.displayFrequency(.immediate)</code>&nbsp;に設定すると、ユーザーが以前にヒントを見ていない場合、ヒントはすぐに表示されます。<br>他の表示頻度も設定することができます。例えば、<code>.hourly</code>&nbsp;に設定すると、システムは1時間に1回以上ヒントが表示されないようにします。<br>ヒントデータの保存場所（ヒントが表示されたかどうか）も&nbsp;<code>.datastoreLocation(.applicationDefault)</code>&nbsp;関数を使用してカスタマイズできます。</p>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7#%E7%89%B9%E5%AE%9A%E3%81%AE%E6%9D%A1%E4%BB%B6%E3%81%8C%E6%BA%80%E3%81%9F%E3%81%95%E3%82%8C%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AB%E3%83%92%E3%83%B3%E3%83%88%E3%82%92%E8%A1%A8%E7%A4%BA"></a>特定の条件が満たされたときにヒントを表示</h2>



<p>特定の条件が満たされた場合にのみヒントを表示することができます。例えば、プレミアム機能のヒントは、ユーザーがプレミアム機能を購入した場合にのみ表示されるべきです。<br>Tipオブジェクトの静的変数を定義することができます。その後、その静的変数に値を割り当てることで、ヒントが表示されるかどうかを制御することができます。</p>



<pre class="wp-block-code"><code>import TipKit

struct PremiumUserOnlyTip: Tip {
    
    @Parameter
    static var isPremiumUser: Bool = false
    
    var image: Image? {
        Image(systemName: "wand.and.rays")
    }
    var title: Text {
        Text("Add an Effect")
    }
    var message: Text? {
        Text("Choose a fun Live Photo effect like Loop or Bounce.")
    }
    
    var rules: &#91;Rule] {
        #Rule(Self.$isPremiumUser) {
            $0 == true
        }
    }
    
}

</code></pre>



<p>アプリの起動時に、静的変数&nbsp;<code>PremiumUserOnlyTip.isPremiumUser</code>&nbsp;を設定することで、ヒントはプレミアムユーザーのみに表示されます。</p>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7#%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%A9%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E8%A8%98%E9%8C%B2%E3%81%97%E3%81%9D%E3%82%8C%E3%81%AB%E5%BF%9C%E3%81%98%E3%81%A6%E3%83%92%E3%83%B3%E3%83%88%E3%82%92%E8%A1%A8%E7%A4%BA"></a>ユーザーのインタラクションを記録し、それに応じてヒントを表示</h2>



<p>ユーザーのインタラクションをカウンターとして記録（アプリを使用した回数など）し、ユーザーが要件を満たしたときのみヒントを表示することができます。</p>



<pre class="wp-block-code"><code>import TipKit

struct UsageFrequencyBasedTip: Tip {
    
    static let numerOfTimesOpened: Event = Event(id: "com.example.TipKit.numberOfTimesOpened")
    
    var image: Image? {
        Image(systemName: "star.fill")
    }
    var title: Text {
        Text("Tap to download HD picture")
    }
    var message: Text? {
        Text("Only for premium users")
    }
    
    var rules: &#91;Rule] {
        #Rule(Self.numerOfTimesOpened) {
            $0.donations.count &gt;= 3
        }
    }
    
}
</code></pre>



<p>上記のコードでは、Eventオブジェクトを使用してアプリを開いた回数を記録します。rulesパラメータでは、Swiftのマクロを使用して、ユーザーがアプリを開いた回数が3回以上の場合にのみヒントが表示されることを示しています。<br>これで、アプリ内で以下のコードを使用してカウンターをインクリメントできます：</p>



<pre class="wp-block-code"><code>Button("tap this button 3 times") {
    Task {
        await UsageFrequencyBasedTip.numerOfTimesOpened.donate()
    }
}
</code></pre>



<p><code>UsageFrequencyBasedTip.numerOfTimesOpened.donate()</code>&nbsp;関数を3回呼び出すと、ヒントが表示されます</p>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E4%BB%98%E3%81%8D%E3%83%92%E3%83%B3%E3%83%88"></a>カスタムアクション付きヒント</h2>



<p>ヒントのアクションボタンを提供することもできます：</p>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F7aa44441-98ff-1329-2ba5-0b6a8f7d7024.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=a4ced2b6aa2c257d8c019a17ba123a96" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="1290" height="701" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F7aa44441-98ff-1329-2ba5-0b6a8f7d7024.jpg" alt="tip-custom-actions.jpg" class="wp-image-310" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F7aa44441-98ff-1329-2ba5-0b6a8f7d7024-300x163.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F7aa44441-98ff-1329-2ba5-0b6a8f7d7024-1024x556.jpg 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F7aa44441-98ff-1329-2ba5-0b6a8f7d7024-768x417.jpg 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F7aa44441-98ff-1329-2ba5-0b6a8f7d7024.jpg 1290w" sizes="auto, (max-width: 1290px) 100vw, 1290px" /></a></figure>



<pre class="wp-block-code"><code>import TipKit

struct TipWithOptions: Tip {
    
    var image: Image? {
        Image(systemName: "star.bubble")
    }
    
    var title: Text {
        Text("Send a Quick Response")
    }
    
    var message: Text? {
        Text("Double-tap a message, then choose a Tapback, like a ♥︎.")
    }
    
    var actions: &#91;Action] {
        return &#91;
            .init(id: "learn-more", title: "Learn more", perform: {
                print("Learn more tapped")
            }),
            .init(id: "enable-feature", title: "Enable magic feature", perform: {
                print("Enable feature tapped")
            })
        ]
    }
    
}</code></pre>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7#%E3%83%92%E3%83%B3%E3%83%88%E3%81%AE%E3%83%87%E3%83%90%E3%83%83%E3%82%B0"></a>ヒントのデバッグ</h2>



<p>以下の関数を使用して、ヒントをデバッグすることができます：<br>ヒントが表示されたかどうかのストレージデータベースをリセット：</p>



<pre class="wp-block-code"><code>try? Tips.resetDatastore()</code></pre>



<p>上記のコードは、Tips/configure(options:)を呼び出す前に呼び出す必要があります。<br>ヒントが以前に表示されたかどうかに関係なく、すべてのヒントを表示することもできます：</p>



<pre class="wp-block-code"><code>Tips.showAllTipsForTesting()</code></pre>



<p>ヒントが以前に表示されたかどうかに関係なく、特定のヒントを表示する：</p>



<pre class="wp-block-code"><code>Tips.showTipsForTesting(&#91;ExampleTip.self])</code></pre>



<p>パラメータで、ヒントのタイプの配列を提供します。<br>すべてのヒントを隠す、または特定のタイプのヒントのみを隠すこともできます：</p>



<pre class="wp-block-code"><code>Tips.hideAllTipsForTesting()
Tips.hideTipsForTesting(_ tips: &#91;Tip.Type])</code></pre>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%82%B3%E3%83%BC%E3%83%89"></a>サンプルコード</h2>



<p>iOS 17が公開された後、サンプルのXcodeプロジェクトをアップロードします。</p>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7#good--ng"></a>Good &amp; NG</h2>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F3b07be64-5fbb-09b5-0026-6a9d368c4885.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=f27eda4ef87c27edbf4c6a6f41824d92" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="720" height="402" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F3b07be64-5fbb-09b5-0026-6a9d368c4885.jpg" alt="tipkit-good.jpeg" class="wp-image-308" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F3b07be64-5fbb-09b5-0026-6a9d368c4885-300x168.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F3b07be64-5fbb-09b5-0026-6a9d368c4885.jpg 720w" sizes="auto, (max-width: 720px) 100vw, 720px" /></a></figure>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2Fd02dcdd1-1dc7-068e-0d90-5b04f9065a60.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=626639c880aa5845456c353b2cec0b40" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="703" height="389" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fd02dcdd1-1dc7-068e-0d90-5b04f9065a60.jpg" alt="tipkit-ng.jpg" class="wp-image-306" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fd02dcdd1-1dc7-068e-0d90-5b04f9065a60-300x166.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fd02dcdd1-1dc7-068e-0d90-5b04f9065a60.jpg 703w" sizes="auto, (max-width: 703px) 100vw, 703px" /></a></figure>



<p>プロモーションのヒントを表示すべきではありません。TipKitを使用してエラーメッセージを表示しないでください。実行可能なアイテム（例：クリック可能な機能ボタン）に関連したヒントのみを表示し、ヒント内に詳細な手順を表示しないでください（NGの例で示されているように。その場合、ユーザーにオプションを提供するためにカスタムアクションボタンを使用するのが最善）。</p>



<p>(source: public Apple session videos)</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>お読みいただき、ありがとうございました。</p>



<p>ニュースレター:&nbsp;<a href="https://blog.mszpro.com/" rel="noreferrer noopener" target="_blank">https://blog.mszpro.com</a></p>



<p>Mastodon/MissKey:&nbsp;<code>@me@mszpro.com</code>&nbsp;<a href="https://sns.mszpro.com/" rel="noreferrer noopener" target="_blank">https://sns.mszpro.com</a></p>



<p><img loading="lazy" decoding="async" alt=":relaxed:" height="20" src="https://cdn.qiita.com/emoji/twemoji/unicode/263a-fe0f.png" width="20">&nbsp;<a href="https://twitter.com/MszPro" rel="noreferrer noopener" target="_blank">Twitter @MszPro</a></p>



<p><img loading="lazy" decoding="async" alt=":relaxed:" height="20" src="https://cdn.qiita.com/emoji/twemoji/unicode/263a-fe0f.png" width="20">&nbsp;個人ウェブサイト&nbsp;<a href="https://mszpro.com/" rel="noreferrer noopener" target="_blank">https://MszPro.com</a><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F33652157-7da9-ee68-3e4d-4b416edfec58.png?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=72b1843d22f83d51fa3fab97a252e91d" target="_blank" rel="noreferrer noopener"></a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7#%E6%B3%A8"></a>注</h2>



<p>上記内容の一部は、Apple社のサンプルコードから引用しています。ライセンスは下記に添付しています：</p>



<pre class="wp-block-code"><code>Copyright © 2023 Apple Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</code></pre>



<p><a href="https://b.hatena.ne.jp/entry/s/qiita.com/mashunzhe/items/016e991fb9020b3eb4c7" rel="noreferrer noopener" target="_blank"></a></p><p>The post <a href="https://mszpro.com/tipkit-ios17-ja">SwiftUIでTipKitを使用してユーザーにヒントを表示（iOS 17、WWDC 2023）</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Using TipKit to display a tip to the user in SwiftUI for onboarding</title>
		<link>https://mszpro.com/ios17-tipkit-swiftui</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Mon, 16 Dec 2024 07:25:23 +0000</pubDate>
				<category><![CDATA[SwiftUI]]></category>
		<category><![CDATA[TipKit]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=293</guid>

					<description><![CDATA[<p>This article is about using the new TipKit framework for iOS 17 to help the user discover various features within your app. This article will cover: This article is primarily for SwiftUI. This article is written with public resources and may contain screenshots from public Apple documentations and example codes. To see how it works, [&#8230;]</p>
<p>The post <a href="https://mszpro.com/ios17-tipkit-swiftui">Using TipKit to display a tip to the user in SwiftUI for onboarding</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></description>
										<content:encoded><![CDATA[<p id="9160">This article is about using the new TipKit framework for iOS 17 to help the user discover various features within your app.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1290" height="701" src="https://static-assets.mszpro.com/2024/12/1EPq81uvZWHL5BTDQEzbfcg.jpg" alt="" class="wp-image-298" srcset="https://static-assets.mszpro.com/2024/12/1EPq81uvZWHL5BTDQEzbfcg-300x163.jpg 300w, https://static-assets.mszpro.com/2024/12/1EPq81uvZWHL5BTDQEzbfcg-1024x556.jpg 1024w, https://static-assets.mszpro.com/2024/12/1EPq81uvZWHL5BTDQEzbfcg-768x417.jpg 768w, https://static-assets.mszpro.com/2024/12/1EPq81uvZWHL5BTDQEzbfcg.jpg 1290w" sizes="auto, (max-width: 1290px) 100vw, 1290px" /></figure>



<p id="e1b1">This article will cover:</p>



<ul class="wp-block-list">
<li>displaying an inline tip</li>



<li>displaying floating tip</li>



<li>display tip based on conditions</li>



<li>display tip based on a counter</li>
</ul>



<p id="e167">This article is primarily for SwiftUI.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p id="ccb9">This article is written with public resources and may contain screenshots from public Apple documentations and example codes. To see how it works, please try it yourself with the&nbsp;<strong>Xcode 15</strong></p>
</blockquote>



<figure class="wp-block-video"><video controls src="https://static-assets.mszpro.com/2024/12/TipKit-SwiftUI-explained_-Showing-tips-conditional-swift-macro-rules-new-in-WWDC-2023-iOS-17.mp4" playsinline class="mcloud-attachment-294"></video></figure>



<h1 class="wp-block-heading" id="ea19">Define a&nbsp;<code>Tip</code>&nbsp;object</h1>



<p id="a97a">A&nbsp;<code>Tip</code>&nbsp;object contains the image, title, description, and the actions for a tip. Here is a simple tip:</p>



<pre class="wp-block-preformatted">import TipKit<br><br>struct HashTagPostButtonTip: Tip {<br>    var image: Image? {<br>        Image(systemName: "star.bubble")<br>    }<br>    var title: Text {<br>        Text("Send a Quick Response")<br>    }<br>    var message: Text? {<br>        Text("Double-tap a message, then choose a Tapback, like a ♥︎.")<br>    }<br>}</pre>



<p id="93c4">In the&nbsp;<code>var asset: Image?</code>, you can provide an optional image to be displayed on the left side of the tip.</p>



<p id="fa1c">In the&nbsp;<code>var title: Text</code>, you will provide a SwiftUI Text for the text you want to show as the title of this tip.</p>



<p id="e9fc">In the&nbsp;<code>var message: Text?</code>, you can provide an optional description string that will be displayed at the bottom of the title.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1290" height="790" src="https://static-assets.mszpro.com/2024/12/1nwMwrwk0f9uh9bqIbaKlog.jpg" alt="" class="wp-image-299" srcset="https://static-assets.mszpro.com/2024/12/1nwMwrwk0f9uh9bqIbaKlog-300x184.jpg 300w, https://static-assets.mszpro.com/2024/12/1nwMwrwk0f9uh9bqIbaKlog-1024x627.jpg 1024w, https://static-assets.mszpro.com/2024/12/1nwMwrwk0f9uh9bqIbaKlog-768x470.jpg 768w, https://static-assets.mszpro.com/2024/12/1nwMwrwk0f9uh9bqIbaKlog.jpg 1290w" sizes="auto, (max-width: 1290px) 100vw, 1290px" /></figure>



<p id="5506">There is also a&nbsp;<code>var rules: [Rule]</code>&nbsp;and the&nbsp;<code>var actions: [Action]</code>parameter, which we will talk about in the next section of this article.</p>



<h1 class="wp-block-heading" id="d298">Displaying a tip</h1>



<p id="99ae">There are 2 ways to display a tip.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="720" height="561" src="https://static-assets.mszpro.com/2024/12/11XeeKELynI75QzvldhmYgQ.jpg" alt="" class="wp-image-301" srcset="https://static-assets.mszpro.com/2024/12/11XeeKELynI75QzvldhmYgQ-300x234.jpg 300w, https://static-assets.mszpro.com/2024/12/11XeeKELynI75QzvldhmYgQ.jpg 720w" sizes="auto, (max-width: 720px) 100vw, 720px" /><figcaption class="wp-element-caption">image from public documentation on developer.apple.com</figcaption></figure>



<h1 class="wp-block-heading" id="1bf8">inline tips</h1>



<p id="5ff2">You can display the tip within your view. The tip view will come with an arrow that points to your feature (for example, a button). You can do that by directly including&nbsp;<code>TipView</code>&nbsp;in your view code:</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1290" height="682" src="https://static-assets.mszpro.com/2024/12/1zYNiB-TgmkSg_UNqC20OMQ.jpg" alt="" class="wp-image-295" srcset="https://static-assets.mszpro.com/2024/12/1zYNiB-TgmkSg_UNqC20OMQ-300x159.jpg 300w, https://static-assets.mszpro.com/2024/12/1zYNiB-TgmkSg_UNqC20OMQ-1024x541.jpg 1024w, https://static-assets.mszpro.com/2024/12/1zYNiB-TgmkSg_UNqC20OMQ-768x406.jpg 768w, https://static-assets.mszpro.com/2024/12/1zYNiB-TgmkSg_UNqC20OMQ.jpg 1290w" sizes="auto, (max-width: 1290px) 100vw, 1290px" /></figure>



<pre class="wp-block-preformatted">import SwiftUI<br>import TipKit<br><br>struct TipWithArrow: View {<br>    var body: some View {<br>        VStack {<br>            <br>            HStack {<br>                TipView(HashTagPostButtonTip(), arrowEdge: .trailing)<br>                <br>                Image(systemName: "number")<br>                    .font(.system(size: 30))<br>                    .foregroundStyle(.white)<br>                    .padding()<br>                    .background { Circle().foregroundStyle(.blue) }<br>            }<br>            <br>        }<br>        .padding()<br>        .task {<br>            try? Tips.configure([<br>                .displayFrequency(.immediate),<br>                .datastoreLocation(.applicationDefault)<br>            ])<br>        }<br>    }<br>}<br><br>#Preview {<br>    TipWithArrow()<br>}</pre>



<p id="0db0">You can use the&nbsp;<code>arrowEdge</code>&nbsp;parameter to decide which direction the arrow points to. Set to&nbsp;<code>.trailing</code>&nbsp;so that the tip will display an arrow point to the right side (meaning the feature button is on the right); or set it to&nbsp;<code>.leading</code>&nbsp;when the feature button is on the left (and the shown tip is on the right side of the button).</p>



<h1 class="wp-block-heading" id="9492">Floating (popover) tips</h1>



<p id="ef85">You can also use a view modifier to attach a floating tip to a button. This is useful for example when you want to show a tip for a navigation button:</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1290" height="740" src="https://static-assets.mszpro.com/2024/12/1OU2pL_DbIDgvWyiq7L7_Cg.jpg" alt="" class="wp-image-300" srcset="https://static-assets.mszpro.com/2024/12/1OU2pL_DbIDgvWyiq7L7_Cg-300x172.jpg 300w, https://static-assets.mszpro.com/2024/12/1OU2pL_DbIDgvWyiq7L7_Cg-1024x587.jpg 1024w, https://static-assets.mszpro.com/2024/12/1OU2pL_DbIDgvWyiq7L7_Cg-768x441.jpg 768w, https://static-assets.mszpro.com/2024/12/1OU2pL_DbIDgvWyiq7L7_Cg.jpg 1290w" sizes="auto, (max-width: 1290px) 100vw, 1290px" /></figure>



<pre class="wp-block-preformatted">struct PopOverTip: View {<br>    <br>    var hashtagButtonTip = HashTagPostButtonTip()<br>    <br>    var body: some View {<br>        VStack {<br>            <br>            Text("Hello world!")<br>            <br>        }<br>        .padding()<br>        .toolbar {<br>            ToolbarItem(placement: .navigationBarTrailing) {<br>                Image(systemName: "number")<br>                    .onTapGesture {<br>                        hashtagButtonTip.invalidate(reason: .actionPerformed)<br>                    }<br>                    .popoverTip(hashtagButtonTip)<br>            }<br>        }<br>        .task {<br>            try? Tips.configure([<br>                .displayFrequency(.immediate),<br>                .datastoreLocation(.applicationDefault)<br>            ])<br>        }<br>    }<br>    <br>}<br><br>#Preview {<br>    PopOverTip()<br>}</pre>



<h1 class="wp-block-heading" id="8212">Tip frequency</h1>



<p id="548d">By default, each tip only shows once.</p>



<p id="8c6c">When you set&nbsp;<code>.displayFrequency(.immediate)</code>, the tip will be immediately shown if the user has not seen the tip before.</p>



<p id="9932">You can also set other display frequencies. For example, if you set it to&nbsp;<code>.hourly</code>, the system makes sure that no more than 1 tip is shown every hour.</p>



<p id="ce2b">You can also customize where the tip data (whether the tips have been shown or not) by using the&nbsp;<code>.datastoreLocation(.applicationDefault)</code>&nbsp;function.</p>



<h1 class="wp-block-heading" id="593e">Show tip when certain condition is met</h1>



<p id="485e">You can only show a tip when certain condition is met. For example, a tip for a premium feature should only be shown if the user has purchased the premium feature.</p>



<p id="45c6">You can define a static variable for the&nbsp;<code>Tip</code>&nbsp;object. Then, you can assign values to that static variable to control whether the tip is shown or not.</p>



<pre class="wp-block-preformatted">import TipKit<br><br>struct PremiumUserOnlyTip: Tip {<br>    <br>    @Parameter<br>    static var isPremiumUser: Bool = false<br>    <br>    var image: Image? {<br>        Image(systemName: "wand.and.rays")<br>    }<br>    var title: Text {<br>        Text("Add an Effect")<br>    }<br>    var message: Text? {<br>        Text("Choose a fun Live Photo effect like Loop or Bounce.")<br>    }<br>    <br>    var rules: [Rule] {<br>        #Rule(Self.$isPremiumUser) {<br>            $0 == true<br>        }<br>    }<br>    <br>}</pre>



<p id="ceb3">On app launch, we can set the static variable&nbsp;<code>isPremiumUser</code>&nbsp;so the tip will only be shown for users who is premium.</p>



<pre class="wp-block-preformatted">PremiumUserOnlyTip.isPremiumUser = isPremiumUser</pre>



<h1 class="wp-block-heading" id="468c">Record user interactions and show tips accordingly</h1>



<p id="c712">You can record user interactions as a counter (like the number of times that the user used your app) and only show tips when the user has met a requirement.</p>



<pre class="wp-block-preformatted">import TipKit<br><br>struct UsageFrequencyBasedTip: Tip {<br>    <br>    static let numerOfTimesOpened: Event = Event(id: "com.example.TipKit.numberOfTimesOpened")<br>    <br>    var image: Image? {<br>        Image(systemName: "star.fill")<br>    }<br>    var title: Text {<br>        Text("Tap to download HD picture")<br>    }<br>    var message: Text? {<br>        Text("Only for premium users")<br>    }<br>    <br>    var rules: [Rule] {<br>        #Rule(Self.numerOfTimesOpened) {<br>            $0.donations.count &gt;= 3<br>        }<br>    }<br>    <br>}</pre>



<p id="bc11">In the above code, we record the number of times user opened the app using an&nbsp;<code>Event</code>&nbsp;object. In the&nbsp;<code>rules</code>&nbsp;parameter, we use a Swift Macro and indicate that the tip will only be shown if the times user opened is larger than or equal to 3.</p>



<p id="b62a">Now, we can increment the counter by using the following code in our app:</p>



<pre class="wp-block-preformatted">Button("tap this button 3 times") {<br>    Task {<br>        await UsageFrequencyBasedTip.numerOfTimesOpened.donate()<br>    }<br>}</pre>



<p id="f6bf">When you call&nbsp;<code>donate()</code>&nbsp;function for 3 times, the tip will be shown.</p>



<h1 class="wp-block-heading" id="aa53">Tip with custom actions</h1>



<p id="8de4">You can also provide your action buttons for a tip:</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1290" height="701" src="https://static-assets.mszpro.com/2024/12/1EPq81uvZWHL5BTDQEzbfcg.jpg" alt="" class="wp-image-298" srcset="https://static-assets.mszpro.com/2024/12/1EPq81uvZWHL5BTDQEzbfcg-300x163.jpg 300w, https://static-assets.mszpro.com/2024/12/1EPq81uvZWHL5BTDQEzbfcg-1024x556.jpg 1024w, https://static-assets.mszpro.com/2024/12/1EPq81uvZWHL5BTDQEzbfcg-768x417.jpg 768w, https://static-assets.mszpro.com/2024/12/1EPq81uvZWHL5BTDQEzbfcg.jpg 1290w" sizes="auto, (max-width: 1290px) 100vw, 1290px" /></figure>



<pre class="wp-block-preformatted">import TipKit<br><br>struct TipWithOptions: Tip {<br>    <br>    var image: Image? {<br>        Image(systemName: "star.bubble")<br>    }<br>    <br>    var title: Text {<br>        Text("Send a Quick Response")<br>    }<br>    <br>    var message: Text? {<br>        Text("Double-tap a message, then choose a Tapback, like a ♥︎.")<br>    }<br>    <br>    var actions: [Action] {<br>        return [<br>            .init(id: "learn-more", title: "Learn more", perform: {<br>                print("Learn more tapped")<br>            }),<br>            .init(id: "enable-feature", title: "Enable magic feature", perform: {<br>                print("Enable feature tapped")<br>            })<br>        ]<br>    }<br>    <br>}</pre>



<h1 class="wp-block-heading" id="a60c">Debugging tips</h1>



<p id="8b14">You can debug your tips by using the following function:</p>



<p id="d017">Reset the storage database for whether tips are shown or not:</p>



<pre class="wp-block-preformatted">try? Tips.resetDatastore()</pre>



<p id="9418">The above code must be called before calling&nbsp;<code>Tips/configure(options:)</code></p>



<p id="0357">You can also show all tips regardless of whether a tip has been shown before:</p>



<pre class="wp-block-preformatted">Tips.showAllTipsForTesting()</pre>



<p id="cdb4">You can show a specific tip regardless of whether it has been shown before:</p>



<pre class="wp-block-preformatted">Tips.showTipsForTesting([ExampleTip.self])</pre>



<p id="1ecb">in the parameter, provide an array of the type of the tips.</p>



<p id="02e6">You can also hide all tips, or just hide a specific type of tip:</p>



<pre class="wp-block-preformatted">Tips.hideAllTipsForTesting()<br>Tips.hideTipsForTesting(_ tips: [Tip.Type])</pre>



<h1 class="wp-block-heading" id="e270">Good vs Not Good</h1>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="720" height="402" src="https://static-assets.mszpro.com/2024/12/1Q2ZGzM0zhRh0Dc0k55vlmw.jpg" alt="" class="wp-image-296" srcset="https://static-assets.mszpro.com/2024/12/1Q2ZGzM0zhRh0Dc0k55vlmw-300x168.jpg 300w, https://static-assets.mszpro.com/2024/12/1Q2ZGzM0zhRh0Dc0k55vlmw.jpg 720w" sizes="auto, (max-width: 720px) 100vw, 720px" /><figcaption class="wp-element-caption">Good Examples. image from public documentation on developer.apple.com</figcaption></figure>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="703" height="389" src="https://static-assets.mszpro.com/2024/12/1mu3M2__oczIRp0z7qwUYig.jpg" alt="" class="wp-image-297" srcset="https://static-assets.mszpro.com/2024/12/1mu3M2__oczIRp0z7qwUYig-300x166.jpg 300w, https://static-assets.mszpro.com/2024/12/1mu3M2__oczIRp0z7qwUYig.jpg 703w" sizes="auto, (max-width: 703px) 100vw, 703px" /><figcaption class="wp-element-caption">NG (Not Good). image from public documentation on developer.apple.com</figcaption></figure>



<p id="c038">vYou should not show promotion tips, do not use TipKit to show error messages, only show tip that is associated with an actionable item (for example, a clickable feature button), and do not show detailed steps within the tip (like shown in the example of the NG)</p><p>The post <a href="https://mszpro.com/ios17-tipkit-swiftui">Using TipKit to display a tip to the user in SwiftUI for onboarding</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></content:encoded>
					
		
		<enclosure url="https://static-assets.mszpro.com/2024/12/TipKit-SwiftUI-explained_-Showing-tips-conditional-swift-macro-rules-new-in-WWDC-2023-iOS-17.mp4" length="53966275" type="video/mp4" />

			</item>
		<item>
		<title>「iOS 17」SwiftUIの新たな19本の機能とビュー（コード例付き）（WWDC 2023）</title>
		<link>https://mszpro.com/ios-17-new-swiftui-ja</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Mon, 16 Dec 2024 07:22:50 +0000</pubDate>
				<category><![CDATA[iOS 17]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<category><![CDATA[WWDC]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=278</guid>

					<description><![CDATA[<p>スクロール位置制御、App Store購入・サブスクリプション表示、NSFW写真分析、SwiftDataデータ管理、Metalシェーダー活用、マップマーカー追加、SFシンボルエフェクト、回転ジェスチャー、インスペクタビュー、#previewプレビュー、foregroundStyle、TipKitヒント表示、Swift Macro対応。</p>
<p>The post <a href="https://mszpro.com/ios-17-new-swiftui-ja">「iOS 17」SwiftUIの新たな19本の機能とビュー（コード例付き）（WWDC 2023）</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">目次</h2>



<ul class="wp-block-list">
<li>スクロールビューで特定の位置までスクロール</li>



<li>App Storeで購入可能なアイテムを表示</li>



<li>App Storeのサブスクリプションを表示</li>



<li>写真がNSFWかどうかを分析</li>



<li>データストレージSwiftDataを使用</li>



<li>SwiftUIのビュースタイルのためにMetalシェーダーを使用</li>



<li>マップにマーカーやその他のコンポーネントを追加</li>



<li>SFシンボルの画像エフェクト（パルス、反転、バウンス、スケール、表示/非表示、トランジション）</li>



<li>回転のジェスチャー</li>



<li>インスペクタビュー(右側に表示されるサイドバー)</li>



<li>新しい#previewプレビューブロック</li>



<li>foregroundStyleスタイルの使用</li>



<li>TipKitを使ったヒントの表示</li>



<li>Swift Macro</li>
</ul>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB"></a>はじめに</h2>



<p>この記事の多くの部分（例えば、SwiftData、センシティブコンテンツ分析など）については、後ほどQiitaの記事でより詳しい解説を書く予定です。</p>



<p>※一般公開されている（一般に開示した情報）WWDC Keynoteの動画と公開Session/Documentationページだけを使ってこの記事を執筆しました。スクリーンショットはWWDCのセッション映像のものを使用しています。Xcodeのベータ版にアクセスできる場合は、自身でコードを実行されることをお勧めします。</p>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#scrollview%E5%86%85%E3%81%A7%E7%89%B9%E5%AE%9A%E3%81%AE%E4%BD%8D%E7%BD%AE%E3%81%BE%E3%81%A7%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B"></a>ScrollView内で特定の位置までスクロールする</h2>



<p>ScrollViewとその内部のスタック構造を使用している場合、<br>特定の行までビューをスクロールすることができます。</p>



<p>まず、.scrollTargetLayout()というビューモディファイアを、ScrollView内に主要な繰り返しコンテンツを含むレイアウトコンテナに追加します。これはVStack、LazyHStack、HStack、またはForEachなどになります。</p>



<pre class="wp-block-code"><code>import SwiftUI

struct ScrollViewToRow: View {
    var body: some View {
        VStack {
            ScrollView {
                ForEach(1..&lt;30, id: \.self) { number in
                    // ...
                }
+                .scrollTargetLayout()
            }
        }
    }
}
</code></pre>



<p>この例では、1から30までのすべての数字をループするためにForEachを使用しています。したがって、このビューモディファイアをForEachに追加します。</p>



<p>次に、scrollPositionビューモディファイアを使用してスクロール位置をバインドします。このビューモディファイアはScrollViewに添付されます。</p>



<pre class="wp-block-code"><code>import SwiftUI

struct ScrollViewToRow: View {
+    @State private var scrollPosition: Int? = 0
    var body: some View {
        VStack {
            ScrollView {
                ForEach(1..&lt;30, id: \.self) { number in
                    // ...
                }
                    .scrollTargetLayout()
            }
+            .scrollPosition(id: $scrollPosition)
        }
    }
}
</code></pre>



<p>オプションのInt型の<a href="https://qiita.com/State">@State</a>変数を使用します。</p>



<p>新しい値を割り当てることでスクロール位置を更新することができます。コードをwithAnimationブロック内に配置すると、スクロールアニメーションが表示されます。</p>



<p>現在のスクロール位置を読み取ることもできます。</p>



<pre class="wp-block-code"><code>import SwiftUI

struct ScrollViewToRow: View {
    
    @State private var scrollPosition: Int? = 0
    
    var body: some View {
        
        VStack {
            
            Text("currently at \(scrollPosition ?? -1)")
            
            Button("Scroll") {
                withAnimation {
                    scrollPosition = 10
                }
            }
            
            ScrollView {
                ForEach(1..&lt;30, id: \.self) { number in
                    HStack {
                        Text(verbatim: number.formatted())
                        Spacer()
                    }
                    .padding()
                    .foregroundStyle(.white)
                    .background {
                        RoundedRectangle(cornerRadius: 10)
                            .foregroundStyle(.teal)
                    }
                }
                    .scrollTargetLayout()
            }
            .scrollPosition(id: $scrollPosition)
            
        }
        .padding()
        
    }
    
}

#Preview {
    ScrollViewToRow()
}
</code></pre>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#app-store%E3%81%AE%E3%82%A2%E3%83%97%E3%83%AA%E5%86%85%E8%B3%BC%E5%85%A5%E8%A3%BD%E5%93%81%E3%82%92%E8%A1%A8%E7%A4%BA"></a>App Storeのアプリ内購入製品を表示</h2>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F9059afe0-2633-6d8f-0504-97a8d702ea62.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=6563b842faaf5b3abd619b0160eb6cff" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="2048" height="1152" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F9059afe0-2633-6d8f-0504-97a8d702ea62.jpg" alt="IMG_1155.JPG" class="wp-image-282" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F9059afe0-2633-6d8f-0504-97a8d702ea62-300x169.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F9059afe0-2633-6d8f-0504-97a8d702ea62-1024x576.jpg 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F9059afe0-2633-6d8f-0504-97a8d702ea62-768x432.jpg 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F9059afe0-2633-6d8f-0504-97a8d702ea62-1536x864.jpg 1536w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F9059afe0-2633-6d8f-0504-97a8d702ea62.jpg 2048w" sizes="auto, (max-width: 2048px) 100vw, 2048px" /></a></figure>



<p>このビューでは、1つの製品を表示しています。<br>複数の商品を紹介するスクロールビューや水平スタックなどを作成することができます。</p>



<p>この話題については後で詳しくQiitaの記事を書きます</p>



<p>App Storeから取得した製品IDを使用して、<code>ProductView</code>を初期化できます。</p>



<p>大きな製品ビュー（上記のBox of Nutrition Pelletsのような）を表示するためには、ビューモディファイア&nbsp;<code>.productViewStyle(.large)</code>&nbsp;を使用します。</p>



<pre class="wp-block-code"><code>ProductView(id: ids.nutritionPelletBox) {
    BoxOfNutritionPelletsIcon()
}
.productViewStyle(.large)
</code></pre>



<p>ProductViewのコードブロック内では、製品のアイコン（たとえば、Image）を提供できます。</p>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#app-store%E3%81%AE%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AE%E3%82%B5%E3%83%96%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E8%A1%A8%E7%A4%BA"></a>App Storeのすべてのサブスクリプションを表示</h2>



<p>Appleから提供されるプレスタイルのビューを使用して、App Storeのサブスクリプションを表示することができます。</p>



<p>これを最も簡単に表示する方法は、SubscriptionStoreView(groupID: birdPassGroupID)を呼び出すだけで、非常にシンプルなビューが表示されます。</p>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2Fbf9950d9-5f77-4327-d85c-679ab7236264.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=3510c9214ce25ba3c866fc2e336e1958" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="2048" height="1152" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fbf9950d9-5f77-4327-d85c-679ab7236264.jpg" alt="IMG_1156.JPG" class="wp-image-288" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fbf9950d9-5f77-4327-d85c-679ab7236264-300x169.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fbf9950d9-5f77-4327-d85c-679ab7236264-1024x576.jpg 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fbf9950d9-5f77-4327-d85c-679ab7236264-768x432.jpg 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fbf9950d9-5f77-4327-d85c-679ab7236264-1536x864.jpg 1536w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fbf9950d9-5f77-4327-d85c-679ab7236264.jpg 2048w" sizes="auto, (max-width: 2048px) 100vw, 2048px" /></a></figure>



<p>この話題については後で詳しくQiitaの記事を書きます</p>



<p>このビューはカスタマイズ可能で、カスタムヘッダー、背景、アイコンを表示できます。</p>



<p>まず、サブスクリプショングループIDを使用してSubscriptionStoreViewを初期化します。visibleRelationshipsでは、アップグレードのみを表示するか、すべてのオプション（ダウングレードオプションを含む）を表示するかを定義できます。</p>



<p>ブロック内では、PassMarketingContent（オプション）がヘッダーとして表示されます。ここでは、アプリのアイコン、サブスクリプショングループ名、サブスクリプションの利点を簡単に説明できます。</p>



<p>また、.subscriptionStoreControlIcon（オプション）ビューモディファイアを使用して、各サブスクリプションアイテムのアイコンを定義することもできます。</p>



<pre class="wp-block-code"><code>SubscriptionStoreView(
    groupID: passGroupID,
    visibleRelationships: showPremiumUpgrade ? .upgrade : .all
) {
    PassMarketingContent(showPremiumUpgrade: showPremiumUpgrade)
#if !os(watchOS)
        .containerBackground(for: .subscriptionStoreFullHeight) {
            SkyBackground()
        }
#endif
}
#if os(iOS)
.storeButton(.visible, for: .redeemCode)
#else
.frame(width: 400, height: 550)
#endif
.subscriptionStoreControlIcon { _, subscriptionInfo in
    Group {
        switch PassStatus(levelOfService: subscriptionInfo.groupLevel) {
            case .premium:
                Image(systemName: "bird")
            case .family:
                Image(systemName: "person.3.sequence")
            default:
                Image(systemName: "wallet.pass")
        }
    }
    .foregroundStyle(.accent)
    .symbolVariant(.fill)
}
#if !os(watchOS)
.backgroundStyle(.clear)
.subscriptionStoreButtonLabel(.multiline)
.subscriptionStorePickerItemBackground(.thinMaterial)
#endif
</code></pre>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#sensitivecontentanalysis%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E7%94%BB%E5%83%8F%E3%82%92%E8%A7%A3%E6%9E%90"></a>SensitiveContentAnalysisを使用して画像を解析</h2>



<p><a href="https://developer.apple.com/documentation/sensitivecontentanalysis">https://developer.apple.com/documentation/sensitivecontentanalysis</a></p>



<p>この話題については後で詳しくQiitaの記事を書きます</p>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F83f306b8-1a33-c2ba-9868-7692350711b7.png?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=a6090847e07281c4fa50ea772b147327" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="1277" height="660" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F83f306b8-1a33-c2ba-9868-7692350711b7.png" alt="rendered2x-1683741577.png" class="wp-image-290" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F83f306b8-1a33-c2ba-9868-7692350711b7-300x155.png 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F83f306b8-1a33-c2ba-9868-7692350711b7-1024x529.png 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F83f306b8-1a33-c2ba-9868-7692350711b7-768x397.png 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F83f306b8-1a33-c2ba-9868-7692350711b7.png 1277w" sizes="auto, (max-width: 1277px) 100vw, 1277px" /></a></figure>



<p>まず、センシティブコンテンツ（NSFW）アナライザーのエンタイトルメントをプロジェクトに追加する必要があります。</p>



<p>イメージアナライザーを初期化します：</p>



<pre class="wp-block-code"><code>let analyzer = SCSensitivityAnalyzer()
</code></pre>



<p>次に、アプリは、ユーザーがセンシティブコンテンツ分析機能をオンにしているかどうかをチェックします。</p>



<p>ユーザーは、システム設定（プライバシーセクション内）でこの機能をオンまたはオフにすることができます。この設定がオフの場合、画像の解析はできません。</p>



<pre class="wp-block-code"><code>switch analyzer.analysisPolicy {
    case .simpleInterventions:
        Label("Simple UI", systemImage: "checkmark")
        Text("Blur the image, and show a button to show the content.")
    case .descriptiveInterventions:
        Label("Detailed UI", systemImage: "checkmark")
        Text("Show a detailed description on how these types of images might affect the user.")
    case .disabled:
        Label("Not enabled", systemImage: "xmark")
        Text("To analyze a photo, turn on the sensitive photo warning first in system settings.")
}
</code></pre>



<p>その後、<code>CGImage</code>オブジェクトに変換することで、画像を解析することができます</p>



<pre class="wp-block-code"><code>func analyzePhoto(input: CGImage) {
    Task {
        let response = try await analyzer.analyzeImage(input)
        self.isImageSensitive = response.isSensitive
    }
}
</code></pre>



<p>以下は、フォトピッカーも含むSwiftUIのコードです。</p>



<pre class="wp-block-code"><code>import SwiftUI
import SensitiveContentAnalysis
import PhotosUI

struct ContentView: View {
    
    let analyzer = SCSensitivityAnalyzer()
    
    @State private var pickedPhotoToAnalyze: PhotosPickerItem?
    @State private var analyzedImage: UIImage?
    @State private var isImageSensitive: Bool?
    
    var body: some View {
        
        Form {
            
            Section("Status") {
                
                VStack(alignment: .leading) {
                    Text("Warning enabled in system settings")
                        .font(.headline)
                    switch analyzer.analysisPolicy {
                        case .simpleInterventions:
                            Label("Simple UI", systemImage: "checkmark")
                            Text("Blur the image, and show a button to show the content.")
                        case .descriptiveInterventions:
                            Label("Detailed UI", systemImage: "checkmark")
                            Text("Show a detailed description on how these types of images might affect the user.")
                        case .disabled:
                            Label("Not enabled", systemImage: "xmark")
                            Text("To analyze a photo, turn on the sensitive photo warning first in system settings.")
                    }
                }
                
            }
            .id(analyzer.analysisPolicy.rawValue)
            
            Section("Image analysis") {
                
                PhotosPicker("Pick a photo to analyze", selection: $pickedPhotoToAnalyze)
                    .onChange(of: pickedPhotoToAnalyze) { newValue in
                        if let newValue {
                            handlePickedImageItem(newValue)
                        }
                    }
                
                if let isImageSensitive {
                    if isImageSensitive {
                        Label("Oh no! What image did you pick?", systemImage: "eye.trianglebadge.exclamationmark")
                            .foregroundStyle(.red)
                    } else {
                        Label("It is OK!", systemImage: "checkmark")
                            .foregroundStyle(.green)
                    }
                }
                
                if let analyzedImage,
                   let isImageSensitive
                {
                    if isImageSensitive {
                        Image(uiImage: analyzedImage)
                            .resizable()
                            .scaledToFit()
                            .frame(height: 230)
                            .blur(radius: 5.0, opaque: true)
                    } else {
                        Image(uiImage: analyzedImage)
                            .resizable()
                            .scaledToFit()
                            .frame(height: 230)
                    }
                }
                
            }
            .disabled(analyzer.analysisPolicy == .disabled)
            
        }
        
    }
    
    func analyzePhoto(input: CGImage) {
        Task {
            let response = try await analyzer.analyzeImage(input)
            self.isImageSensitive = response.isSensitive
        }
    }
    
    func handlePickedImageItem(_ newValue: PhotosPickerItem) {
        self.isImageSensitive = nil
        self.analyzedImage = nil
        newValue.loadTransferable(type: Data.self) { result in
            switch result {
                case .success(let success):
                    if let success,
                       let imageObj = UIImage(data: success),
                       let cgImageObj = imageObj.cgImage
                    {
                        self.analyzedImage = imageObj
                        self.analyzePhoto(input: cgImageObj)
                    }
                case .failure(let failure):
                    return
            }
        }
    }
    
}

#Preview {
    ContentView()
}
</code></pre>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#swiftdata"></a>SwiftData</h2>



<p>この話題については後で詳しくQiitaの記事を書きます</p>



<p>SwiftDataはCore Dataを基盤に構築されています（ただし、Core Dataと組み合わせて使用する場合は、ハッシュをチェックするための追加の手順が必要です）。</p>



<p>以下の53行のコードで、保存されるデータの構造を定義し、コンテキストを作成し、新しいレコードを追加するためのSwiftUIビュー、レコードを表示し、削除するためのビューを作成しています。</p>



<pre class="wp-block-code"><code>import SwiftUI
import SwiftData

@Model
class Record {
    
    var id: UUID
    var timestamp: Date
    
    init(timestamp: Date) {
        self.id = UUID()
        self.timestamp = timestamp
    }
    
}

struct SwiftDataDemo: View {
    var body: some View {
        RecordsList()
            .modelContainer(for: &#91;Record.self])
    }
}

struct RecordsList: View {
    @Environment(\.modelContext) private var modelContext
    
    @Query(sort: \.timestamp, order: .forward, animation: .snappy)
    var storedRecords: &#91;Record]
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(storedRecords) { record in
                    Text(record.timestamp, format: .dateTime)
                }
                .onDelete(perform: { indexSet in
                    indexSet.forEach({ modelContext.delete(storedRecords&#91;$0]) })
                })
            }
            .toolbar {
                Button ("Add") {
                    let location = Record(timestamp: Date())
                    modelContext.insert(location)
                }
            }
        }
    }
}

#Preview {
    SwiftDataDemo()
}
</code></pre>



<p>上記のコードにおいて、以下のコードでデータ構造を定義しています：</p>



<pre class="wp-block-code"><code>@Model
class Record {
    
    var id: UUID
    var timestamp: Date
    
    init(timestamp: Date) {
        self.id = UUID()
        self.timestamp = timestamp
    }
    
}
</code></pre>



<p>ビューにストレージをアタッチします。まだ存在しない場合は、新しいストレージが作成されます。</p>



<pre class="wp-block-code"><code>struct SwiftDataDemo: View {
    var body: some View {
        RecordsList()
            .modelContainer(for: &#91;Record.self])
    }
}
</code></pre>



<p>上記のビューモディファイアには、さらにinitパラメータがあり、例えば、データを自動的に保存するかどうか、完了時にコードを実行するかどうかなどを制御することができます。</p>



<p>また、iCloudを有効にし、iCloudコンテナを作成することも可能です。データはiCloudに同期されます</p>



<p>データのクエリには&nbsp;<code>@Query</code>&nbsp;を使用することができます</p>



<pre class="wp-block-code"><code>@Query(sort: \.timestamp, order: .forward, animation: .snappy)
var storedRecords: &#91;Record]
</code></pre>



<p>レコードを削除するには、データベースを読む<a href="https://qiita.com/Environmental">@Environmental</a>変数を作成します。</p>



<pre class="wp-block-code"><code>@Environment(\.modelContext) private var modelContext
</code></pre>



<p>デフォルトでは、変更内容は自動的に保存されます。</p>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#shaderlibrary%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E3%83%93%E3%83%A5%E3%83%BC%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E3%81%ABmetal%E3%82%92%E9%81%A9%E7%94%A8"></a>ShaderLibraryを使用して、ビュースタイルにMetalを適用</h2>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F4df9730a-9383-32d3-4845-fd03b87dbef6.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=fe6bbb4b6ada872df32db76bc5774935" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="2560" height="1203" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F4df9730a-9383-32d3-4845-fd03b87dbef6-scaled.jpg" alt="Fx82SziaEAE-Fpy.jpeg" class="wp-image-280" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F4df9730a-9383-32d3-4845-fd03b87dbef6-300x141.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F4df9730a-9383-32d3-4845-fd03b87dbef6-1024x481.jpg 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F4df9730a-9383-32d3-4845-fd03b87dbef6-768x361.jpg 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F4df9730a-9383-32d3-4845-fd03b87dbef6-1536x722.jpg 1536w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F4df9730a-9383-32d3-4845-fd03b87dbef6-2048x962.jpg 2048w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F4df9730a-9383-32d3-4845-fd03b87dbef6-scaled.jpg 2560w" sizes="auto, (max-width: 2560px) 100vw, 2560px" /></a></figure>



<p>まず、シェーダーの形状を定義する.metal定義ファイルを作成します。</p>



<pre class="wp-block-code"><code>//  angledFill.metal

#include &lt;metal_stdlib&gt;
using namespace metal;

&#91;&#91; stitchable ]] half4
angledFill(float2 position, float width, float angle, half4 color)
{
    float pMagnitude = sqrt(position.x * position.x + position.y * position.y);
    float pAngle = angle +
    (position.x == 0.0f ? (M_PI_F / 2.0f) : atan(position.y / position.x));
    float rotatedX = pMagnitude * cos(pAngle);
    float rotatedY = pMagnitude * sin(pAngle);
    return (color + color * fmod(abs(rotatedX + rotatedY), width) / width) / 2;
}

</code></pre>



<p>次に、上記の.metal定義内の関数（angleFillと呼ばれる）を参照する新しいShaderシェーダーを作成します。</p>



<pre class="wp-block-code"><code>var stripes: Shader {
    ShaderLibrary.angledFill(
        .float(10),
        .float(90),
        .color(.blue)
    )
}
</code></pre>



<p>これで、<code>.foregroundStyle</code>&nbsp;ビューモディファイアを使って、テキストにShaderシェーダースタイルを適用できるようになりました。</p>



<pre class="wp-block-code"><code>Text("Furdinand").font(.system(size: 50).bold()).foregroundStyle(stripes) + Text(" is a good dog.").font(.system(size: 50).bold())
</code></pre>



<p>SwiftUIで異なるテキストコンポーネントを連結させることができます。</p>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#foregroundstyle%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6"></a>.foregroundStyleについて、</h2>



<p><a href="https://developer.apple.com/documentation/swiftui/view/foregroundstyle(_:)">https://developer.apple.com/documentation/swiftui/view/foregroundstyle(_:)</a></p>



<p>.foregroundStyleビューモディファイアを使用して、<br>SwiftUIビューに色またはMetalスタイルを設定することができます。</p>



<p>このビューモディファイアはiOS 15から使用可能ですが、iOS 17では入力パラメータとしてシェーダーを使用することができます。</p>



<h3 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#-ios-15"></a>&gt;= iOS 15</h3>



<p>iOS 15以降、.foregroundStyleを使用してビューの色やグラデーションを設定することができます。</p>



<pre class="wp-block-code"><code>Text("Furdinand")
    .font(.system(size: 50).bold())
    .foregroundStyle(.blue)
</code></pre>



<pre class="wp-block-code"><code>Text("Hello world")
    .foregroundStyle(.linearGradient(colors: &#91;.teal, .blue], startPoint: .top, endPoint: .bottom))

</code></pre>



<h3 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#foregroundcolor%E3%81%AF%E9%9D%9E%E6%8E%A8%E5%A5%A8-will-be-deprecated-%E3%81%AB%E3%81%AA%E3%82%8B%E3%82%88%E3%81%86%E3%81%A7%E3%81%99"></a>.foregroundColor`は非推奨 (will be deprecated) になるようです。</h3>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2Fcdb5e5ca-02f9-7026-dbbf-247f2a544d25.png?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=e5c2124b9698cc74cd320b812fe5e7d8" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="1072" height="358" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fcdb5e5ca-02f9-7026-dbbf-247f2a544d25.png" alt="スクリーンショット 2023-06-17 15.19.27.png" class="wp-image-281" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fcdb5e5ca-02f9-7026-dbbf-247f2a544d25-300x100.png 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fcdb5e5ca-02f9-7026-dbbf-247f2a544d25-1024x342.png 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fcdb5e5ca-02f9-7026-dbbf-247f2a544d25-768x256.png 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fcdb5e5ca-02f9-7026-dbbf-247f2a544d25.png 1072w" sizes="auto, (max-width: 1072px) 100vw, 1072px" /></a></figure>



<figure class="wp-block-embed"><div class="wp-block-embed__wrapper">
https://qiita.com/embed-contents/link-card#qiita-embed-content__a82b23a9bd3189450f7f26e4c14933d2
</div></figure>



<p>もしiOS 14以前のデバイス向けのアプリをターゲットにしている場合、カスタムのビューモディファイアを作成することができます。一方、iOS 15以降のデバイス向けのアプリをターゲットにしている場合は、新しいforegroundStyleビューモディファイアにコードを置き換えることができます。</p>



<p>カスタムのビューモディファイア:&nbsp;<a href="https://gist.github.com/mszpro/fb6bb8a95376402daf433220de222389" rel="noreferrer noopener" target="_blank">https://gist.github.com/mszpro/fb6bb8a95376402daf433220de222389</a></p>



<h3 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#-ios-17"></a>&gt;= iOS 17</h3>



<p>iOS 17では入力パラメータとしてシェーダーを使用することができます。</p>



<pre class="wp-block-code"><code>var stripes: Shader {
    ShaderLibrary.angledFill(
        .float(10),
        .float(90),
        .color(.blue)
    )
}

Text("Furdinand")
    .font(.system(size: 50).bold())
    .foregroundStyle(stripes)
</code></pre>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#swiftui%E3%81%A7mapkit%E3%81%AE%E3%83%9E%E3%83%83%E3%83%97%E3%82%92%E5%88%B6%E5%BE%A1"></a>SwiftUIでMapKitのマップを制御</h2>



<p>以下は、WWDCのセッション動画からのスクリーンショットです。</p>



<p>Markerや図形（円、折れ線、多角形）を簡単に適用することができます。<br>また、<a href="https://qiita.com/Binding">@Binding</a>変数を使用することで、現在どのMarkerが選択されているかを確認することができます。</p>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F556e4687-fe6b-9d67-5908-d9b65bc27ab0.png?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=6df69c10cf5f232cde838b9fcc59e2fb" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="1274" height="710" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F556e4687-fe6b-9d67-5908-d9b65bc27ab0.png" alt="スクリーンショット 2023-06-16 12.01.12.png" class="wp-image-289" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F556e4687-fe6b-9d67-5908-d9b65bc27ab0-300x167.png 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F556e4687-fe6b-9d67-5908-d9b65bc27ab0-1024x571.png 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F556e4687-fe6b-9d67-5908-d9b65bc27ab0-768x428.png 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F556e4687-fe6b-9d67-5908-d9b65bc27ab0.png 1274w" sizes="auto, (max-width: 1274px) 100vw, 1274px" /></a></figure>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F0f46bdac-7656-d275-4d88-fe1f9000a1f4.png?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=9fb95a7cc95106ddaa09a0c29fd7c3d5" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="1280" height="716" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F0f46bdac-7656-d275-4d88-fe1f9000a1f4.png" alt="スクリーンショット 2023-06-16 12.01.19.png" class="wp-image-285" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F0f46bdac-7656-d275-4d88-fe1f9000a1f4-300x168.png 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F0f46bdac-7656-d275-4d88-fe1f9000a1f4-1024x573.png 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F0f46bdac-7656-d275-4d88-fe1f9000a1f4-768x430.png 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F0f46bdac-7656-d275-4d88-fe1f9000a1f4.png 1280w" sizes="auto, (max-width: 1280px) 100vw, 1280px" /></a></figure>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F9180fb24-5ce3-a290-76f2-c735e0b49f64.png?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=176cc1235efebb83014a87742acc8864" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="1288" height="732" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F9180fb24-5ce3-a290-76f2-c735e0b49f64.png" alt="スクリーンショット 2023-06-16 12.01.52.png" class="wp-image-291" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F9180fb24-5ce3-a290-76f2-c735e0b49f64-300x170.png 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F9180fb24-5ce3-a290-76f2-c735e0b49f64-1024x582.png 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F9180fb24-5ce3-a290-76f2-c735e0b49f64-768x436.png 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F9180fb24-5ce3-a290-76f2-c735e0b49f64.png 1288w" sizes="auto, (max-width: 1288px) 100vw, 1288px" /></a></figure>



<p>この話題については後で詳しくQiitaの記事を書きます</p>



<p><a href="https://developer.apple.com/wwdc23/10043">https://developer.apple.com/wwdc23/10043</a></p>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#sf%E3%82%B7%E3%83%B3%E3%83%9C%E3%83%AB%E3%82%A8%E3%83%95%E3%82%A7%E3%82%AF%E3%83%88"></a>SFシンボルエフェクト</h2>



<p>SwiftUIでSFシンボル画像に多くの視覚効果を加えることができる</p>



<h3 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#%E3%83%91%E3%83%AB%E3%82%B9"></a>パルス</h3>



<p>シンボルの不透明度をアニメーションで表示します。</p>



<pre class="wp-block-code"><code>Image(systemName: "rectangle.inset.filled.and.person.filled")
            .symbolEffect(.pulse)
            .frame(maxWidth: .infinity)
            .font(.system(size: 50))
            .symbolRenderingMode(.multicolor)
</code></pre>



<h3 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#%E3%83%AA%E3%83%90%E3%83%BC%E3%82%B7%E3%83%B3%E3%82%B0"></a>リバーシング</h3>



<p>シンボルをレイヤーごとにアニメーションさせることができます。例えば、WiFi接続のアニメーションを作成することができます。</p>



<pre class="wp-block-code"><code>Image(systemName: "wifi")
    .symbolEffect(.variableColor.iterative.reversing)
    .font(.system(size: 50))
    .symbolRenderingMode(.multicolor)
</code></pre>



<h3 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#%E3%83%90%E3%82%A6%E3%83%B3%E3%82%B9%E3%83%8F%E3%83%BC%E3%83%88%E3%83%93%E3%83%BC%E3%83%88%E5%8A%B9%E6%9E%9C"></a>バウンス（ハートビート効果）</h3>



<p>画像のスケールをアニメーションで変化させます。画像を拡大し、縮小します。心臓の鼓動のようです。</p>



<pre class="wp-block-code"><code>Image(systemName: "arrow.down.circle")
    .symbolEffect(.bounce, value: simulatedDownloadPercentage)
    .font(.system(size: 50))
    .symbolRenderingMode(.multicolor)
</code></pre>



<h3 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#%E6%8B%A1%E5%A4%A7%E7%B8%AE%E5%B0%8F"></a>拡大・縮小</h3>



<p>上記と同様に、bool値を使ってシンボルを拡大・縮小することもできます。以下のサンプルコードでは、<code>simulatedDownloadPercentage</code>&nbsp;が偶数である場合に画像を拡大表示しています。</p>



<pre class="wp-block-code"><code>Image(systemName: "bubble.left.and.bubble.right.fill")
    .symbolEffect(.scale.up, isActive: simulatedDownloadPercentage % 2 == 0)
    .font(.system(size: 50))
    .symbolRenderingMode(.multicolor)
</code></pre>



<h3 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#%E8%A1%A8%E7%A4%BA%E9%9D%9E%E8%A1%A8%E7%A4%BA"></a>表示・非表示</h3>



<pre class="wp-block-code"><code>Image(systemName: "cloud.sun.rain.fill")
    .symbolEffect(.disappear, isActive: simulatedDownloadPercentage % 2 == 0)
    .font(.system(size: 50))
    .symbolRenderingMode(.multicolor)
</code></pre>



<h3 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#2%E3%81%A4%E3%81%AE%E3%82%B7%E3%83%B3%E3%83%9C%E3%83%AB%E9%96%93%E3%81%AE%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3%E5%8A%B9%E6%9E%9C"></a>2つのシンボル間のトランジション効果</h3>



<p>1つのシンボルから別のシンボルへの切り替え時に、トランジション効果を追加することができます。例えば、再生ボタンで、再生アイコンと一時停止アイコンを切り替えるような場合に使用できます。</p>



<pre class="wp-block-code"><code>VStack {
    Image(systemName: runToggle ? "play.fill" : "pause.fill")
        .contentTransition(.symbolEffect(.replace.downUp))
        .font(.system(size: 50))
        .symbolRenderingMode(.multicolor)
    Button("Toggle status") {
        runToggle.toggle()
    }
}
</code></pre>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#%E5%9B%9E%E8%BB%A2%E3%82%B8%E3%82%A7%E3%82%B9%E3%83%81%E3%83%A3%E3%83%BC"></a>回転ジェスチャー</h2>



<p>回転ジェスチャーを認識することで、ビューの回転を監視することができます。</p>



<pre class="wp-block-code"><code>struct RotationTesting: View {
    @State private var angleRotated = Angle(degrees: 0.0)

    var body: some View {
        Rectangle()
            .frame(width: 200, height: 200, alignment: .center)
            .rotationEffect(angleRotated)
            .gesture(
                RotateGesture()
                    .onChanged { value in
                        angleRotated = value.rotation
                    }
            )
    }
}
</code></pre>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%9A%E3%82%AF%E3%82%BF%E3%83%BC%E3%83%93%E3%83%A5%E3%83%BC%E5%8F%B3%E5%81%B4%E3%81%AE%E3%82%B5%E3%82%A4%E3%83%89%E3%83%90%E3%83%BC%E3%82%92%E8%A1%A8%E7%A4%BA"></a>インスペクタービュー（右側のサイドバー）を表示</h2>



<p>インスペクターは右側に表示され、通常、ユーザーがアイテムの詳細を表示するために使用されます。</p>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2Fe060a408-0610-e2a6-cf06-c17dc333a5c0.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=8682a0fea3b80562e4f1fbd8e5a5b296" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="2560" height="1444" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fe060a408-0610-e2a6-cf06-c17dc333a5c0-scaled.jpg" alt="inspector.jpg" class="wp-image-283" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fe060a408-0610-e2a6-cf06-c17dc333a5c0-300x169.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fe060a408-0610-e2a6-cf06-c17dc333a5c0-1024x577.jpg 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fe060a408-0610-e2a6-cf06-c17dc333a5c0-768x433.jpg 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fe060a408-0610-e2a6-cf06-c17dc333a5c0-1536x866.jpg 1536w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fe060a408-0610-e2a6-cf06-c17dc333a5c0-2048x1155.jpg 2048w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302Fe060a408-0610-e2a6-cf06-c17dc333a5c0-scaled.jpg 2560w" sizes="auto, (max-width: 2560px) 100vw, 2560px" /></a></figure>



<pre class="wp-block-code"><code>public struct ContentView: View {
    @State private var state = AppState()
    @State private var presented = true

    public var body: some View {
        AnimalTable(state: $state)
            .inspector(isPresented: $presented) {
                AnimalInspectorForm(animal: $state.binding())
                    .inspectorColumnWidth(
                        min: 200, ideal: 300, max: 400)
                    .toolbar {
                        Spacer()
                        Button {
                            presented.toggle()
                        } label: {
                            Label("Toggle Inspector", systemImage: "info.circle")
                        }
                    }
            }
    }
}
</code></pre>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#%E7%B0%A1%E5%8D%98%E3%81%AB%E3%83%97%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC"></a>簡単にプレビュー</h2>



<p>SwiftUIのビューをプレビューするために#previewを記述することができます。<br>また、#preview内で変数を直接追加することもできます。</p>



<pre class="wp-block-code"><code>#Preview {
    ContentView()
}
</code></pre>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#tipkit%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%B8%E3%81%AE%E3%83%81%E3%83%83%E3%83%97%E8%A1%A8%E7%A4%BA%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6"></a>TipKitを使ったユーザーへのチップ表示について</h2>



<p><a href="https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7">https://qiita.com/mashunzhe/items/016e991fb9020b3eb4c7</a></p>



<p><a href="https://developer.apple.com/videos/play/wwdc2023/10229">https://developer.apple.com/videos/play/wwdc2023/10229</a></p>



<p><code>TipKit</code>を使って、ユーザーにヒントを表示し、どこにどんな機能があるかを理解させることができます。</p>



<p>iOSベータ1では、ヒントを表示する際に問題があるかもしれませんが、<br>後でコードを更新します。</p>



<p><a href="https://developer.apple.com/forums/thread/731073">https://developer.apple.com/forums/thread/731073</a></p>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F6f6c7596-f8d9-8ad8-1d25-63eb180f5105.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=76a8929122c7cd4da819065185cbc15c" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="1630" height="890" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F6f6c7596-f8d9-8ad8-1d25-63eb180f5105.jpg" alt="Fx53LuLakAENWgH.jpeg" class="wp-image-286" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F6f6c7596-f8d9-8ad8-1d25-63eb180f5105-300x164.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F6f6c7596-f8d9-8ad8-1d25-63eb180f5105-1024x559.jpg 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F6f6c7596-f8d9-8ad8-1d25-63eb180f5105-768x419.jpg 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F6f6c7596-f8d9-8ad8-1d25-63eb180f5105-1536x839.jpg 1536w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F6f6c7596-f8d9-8ad8-1d25-63eb180f5105.jpg 1630w" sizes="auto, (max-width: 1630px) 100vw, 1630px" /></a></figure>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F5916e6f2-94f6-160d-cccc-390b5f3189fa.jpeg?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=5ff75f7df516cb5fb0131933987c7045" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="1200" height="671" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5916e6f2-94f6-160d-cccc-390b5f3189fa.jpg" alt="Fx9t6XFX0AElPzo.jpeg" class="wp-image-287" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5916e6f2-94f6-160d-cccc-390b5f3189fa-300x168.jpg 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5916e6f2-94f6-160d-cccc-390b5f3189fa-1024x573.jpg 1024w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5916e6f2-94f6-160d-cccc-390b5f3189fa-768x429.jpg 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F5916e6f2-94f6-160d-cccc-390b5f3189fa.jpg 1200w" sizes="auto, (max-width: 1200px) 100vw, 1200px" /></a></figure>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#macros"></a>Macros</h2>



<figure class="wp-block-embed"><div class="wp-block-embed__wrapper">
https://qiita.com/embed-contents/link-card#qiita-embed-content__7cc08dca7bcc19b7a5b67b50e4b9dbf0
</div></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>お読みいただき、ありがとうございました。</p>



<p><img loading="lazy" decoding="async" alt=":relaxed:" height="20" src="https://cdn.qiita.com/emoji/twemoji/unicode/263a-fe0f.png" width="20">&nbsp;<a href="https://twitter.com/MszPro" rel="noreferrer noopener" target="_blank">Twitter @MszPro</a></p>



<p><img loading="lazy" decoding="async" alt=":relaxed:" height="20" src="https://cdn.qiita.com/emoji/twemoji/unicode/263a-fe0f.png" width="20">&nbsp;個人ウェブサイト&nbsp;<a href="https://mszpro.com/" rel="noreferrer noopener" target="_blank">https://MszPro.com</a></p>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F8b74bbc1-f44d-8d7d-1573-74a16232fe1f.png?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=228871d49035ded66acfc3b9fb30ae26" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="768" height="768" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F8b74bbc1-f44d-8d7d-1573-74a16232fe1f.png" alt="" class="wp-image-284" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F8b74bbc1-f44d-8d7d-1573-74a16232fe1f-300x300.png 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F8b74bbc1-f44d-8d7d-1573-74a16232fe1f-150x150.png 150w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F8b74bbc1-f44d-8d7d-1573-74a16232fe1f.png 768w" sizes="auto, (max-width: 768px) 100vw, 768px" /></a></figure>



<figure class="wp-block-image"><a href="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F635330%2F33652157-7da9-ee68-3e4d-4b416edfec58.png?ixlib=rb-4.0.0&amp;auto=format&amp;gif-q=60&amp;q=75&amp;s=72b1843d22f83d51fa3fab97a252e91d" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" width="800" height="600" src="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F33652157-7da9-ee68-3e4d-4b416edfec58.png" alt="" class="wp-image-279" srcset="https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F33652157-7da9-ee68-3e4d-4b416edfec58-300x225.png 300w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F33652157-7da9-ee68-3e4d-4b416edfec58-768x576.png 768w, https://static-assets.mszpro.com/2024/12/https3A2F2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com2F02F6353302F33652157-7da9-ee68-3e4d-4b416edfec58.png 800w" sizes="auto, (max-width: 800px) 100vw, 800px" /></a></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading"><a href="https://qiita.com/mashunzhe/items/3c2ee9d6b8a6065fd117#%E6%B3%A8"></a>注</h2>



<p>上記内容の一部は、Apple社のサンプルコードから引用しています。ライセンスは下記に添付しています：</p>



<pre class="wp-block-code"><code>Copyright © 2023 Apple Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</code></pre><p>The post <a href="https://mszpro.com/ios-17-new-swiftui-ja">「iOS 17」SwiftUIの新たな19本の機能とビュー（コード例付き）（WWDC 2023）</a> first appeared on <a href="https://mszpro.com">MszPro・株式会社Smartソフト</a>.</p>]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/

Page Caching using Disk: Enhanced 
Lazy Loading (feed)
Database Caching using Disk

Served from: mszpro.com @ 2025-07-07 14:59:23 by W3 Total Cache
-->