<?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>AsterKit Software Limited</title>
	<atom:link href="https://mszpro.com/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>Sat, 10 May 2025 14:14:16 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>https://static-assets.mszpro.com/2025/09/cropped-an-computer-coding-32x32.png</url>
	<title>AsterKit Software Limited</title>
	<link>https://mszpro.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>【Ghost Town VR鬼镇｜大结局】打败红衣主教，释放无数灵魂，最终并没有打败恶魔大boss？｜2025最新解谜VR游戏大作 by Fireproof Studio｜中文，边讲边玩</title>
		<link>https://mszpro.com/https-mszpro-com-ghost-town-cn-5</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Sat, 10 May 2025 13:16:16 +0000</pubDate>
				<category><![CDATA[Ghost Town 鬼镇 VR游戏 2025]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=805</guid>

					<description><![CDATA[<p>剧情概括 【上集回顾】 上一集中，我们追踪到那件神秘物体属于一座被火焚毁、如今改建为地铁站的教堂遗迹。地铁站因连串诡事早已关闭，真正的教堂秘密则深埋地下。为探真相，我循着一缕幽魂的指引潜入遗址最深处。 【教堂地下的红衣主教实验室】 幽魂原是女红衣主教 Nettie Burke 的随从——他曾致信政府警告主教的禁忌研究，却被她发现，惨遭折磨并被永困于此。破解重重谜题后，我终将其灵魂解放，却在更深处目睹弟弟 Adam 正重演主教的黑暗仪式。 【兄弟对峙与恶魔面具】 劝说无果，Adam 戴上面具，嗓音瞬变恶魔低语，黑雾扑面欲强迫我也戴上面具。危急关头，Adam 觉醒抵抗，将面具摔碎；恶魔被逼退，我却被卷入地狱边缘。 【Angela 的援手与通灵镜逃生】 灯塔相遇的 Angela 再度现身，带我借卧室通灵镜返回公寓。室友 Rina 倡议借邻居亡灵之力开启“黑色之门 Black Doorway”，却在仪式中触发幻境：房间扭曲、怨声四起，皆为恶魔造像欲诱我堕落。我识破迷障，摘下 Burke 面具，救赎众灵。 【现身的 Dantalion 与新的契约】 顷刻，一位面具缔造者——恶魔 Dantalion——现身暗影。回到屋顶，我与 Adam 重逢，却得知他已与 Dantalion 达成协议。猩红之门自空中撕开，Adam 迈入其间，身影消失。 【剧场再访・突入终幕】 数日后，我与 Rina 重返序章的剧院欲解放 Catherine 灵魂，却只见一扇未知大门。跨入门内，故事戛然而止——真正的噩梦，才刚刚开始。 观看全集 第一集：和弟弟去剧院，弟弟突然间触碰灵魂导致驱魔失败，释放灯塔灵魂，Angela寻找弟弟下落 https://www.youtube.com/watch?v=rI8kKRPUnns&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=4 第二集：用室友Rina的电脑破译Angela手绘标志，找到奇异的Black Doorway魔咒符号。 https://www.youtube.com/watch?v=lwyAwWz0pgI&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=3 第三集：潜入魔法术士豪宅触摸每一件魔法物品，被关进机关盒子，逃出并获得奇怪的物品。 https://www.youtube.com/watch?v=0UeooYXCN3g&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=2 第四集：和室友Rina一起破解魔法物品，进入废弃的英国地铁站寻找进行过恶魔仪式的教堂地下。 https://www.youtube.com/watch?v=lHv8x_N2W6c&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=1 第五集/大结局（本视频）：解谜地下教堂，发现被恶魔囚禁的灵魂，克服内心的恐惧，击败红衣主教释放灵魂，但最终未能打败大boss，弟弟Adam和恶魔达成协议，游戏全集结束。 https://www.youtube.com/watch?v=HssRQbcsJBI&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=5</p>
<p>The post <a href="https://mszpro.com/https-mszpro-com-ghost-town-cn-5">【Ghost Town VR鬼镇｜大结局】打败红衣主教，释放无数灵魂，最终并没有打败恶魔大boss？｜2025最新解谜VR游戏大作 by Fireproof Studio｜中文，边讲边玩</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></description>
										<content:encoded><![CDATA[<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe title="【Ghost Town 鬼镇｜大结局】打败红衣主教，释放无数灵魂，最终并没有打败恶魔大boss？｜2025最新解谜VR游戏大作【by Fireproof Studio】中文，边讲边玩" width="500" height="281" src="https://www.youtube.com/embed/HssRQbcsJBI?list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<h2 class="wp-block-heading">剧情概括</h2>



<p>【上集回顾】</p>



<p> 上一集中，我们追踪到那件神秘物体属于一座被火焚毁、如今改建为地铁站的教堂遗迹。地铁站因连串诡事早已关闭，真正的教堂秘密则深埋地下。为探真相，我循着一缕幽魂的指引潜入遗址最深处。 </p>



<p>【教堂地下的红衣主教实验室】 </p>



<p>幽魂原是女红衣主教 Nettie Burke 的随从——他曾致信政府警告主教的禁忌研究，却被她发现，惨遭折磨并被永困于此。破解重重谜题后，我终将其灵魂解放，却在更深处目睹弟弟 Adam 正重演主教的黑暗仪式。 </p>



<p>【兄弟对峙与恶魔面具】 劝说无果，Adam 戴上面具，嗓音瞬变恶魔低语，黑雾扑面欲强迫我也戴上面具。危急关头，Adam 觉醒抵抗，将面具摔碎；恶魔被逼退，我却被卷入地狱边缘。</p>



<p> 【Angela 的援手与通灵镜逃生】</p>



<p> 灯塔相遇的 Angela 再度现身，带我借卧室通灵镜返回公寓。室友 Rina 倡议借邻居亡灵之力开启“黑色之门 Black Doorway”，却在仪式中触发幻境：房间扭曲、怨声四起，皆为恶魔造像欲诱我堕落。我识破迷障，摘下 Burke 面具，救赎众灵。 </p>



<p>【现身的 Dantalion 与新的契约】 </p>



<p>顷刻，一位面具缔造者——恶魔 Dantalion——现身暗影。回到屋顶，我与 Adam 重逢，却得知他已与 Dantalion 达成协议。猩红之门自空中撕开，Adam 迈入其间，身影消失。 </p>



<p>【剧场再访・突入终幕】</p>



<p> 数日后，我与 Rina 重返序章的剧院欲解放 Catherine 灵魂，却只见一扇未知大门。跨入门内，故事戛然而止——真正的噩梦，才刚刚开始。</p>



<h2 class="wp-block-heading">观看全集</h2>



<p>第一集：和弟弟去剧院，弟弟突然间触碰灵魂导致驱魔失败，释放灯塔灵魂，Angela寻找弟弟下落 <a href="https://www.youtube.com/watch?v=rI8kKRPUnns&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=4">https://www.youtube.com/watch?v=rI8kKRPUnns&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=4</a></p>



<p> 第二集：用室友Rina的电脑破译Angela手绘标志，找到奇异的Black Doorway魔咒符号。</p>



<p><a href="https://www.youtube.com/watch?v=lwyAwWz0pgI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=3" title="">https://www.youtube.com/watch?v=lwyAwWz0pgI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=3 </a></p>



<p>第三集：潜入魔法术士豪宅触摸每一件魔法物品，被关进机关盒子，逃出并获得奇怪的物品。</p>



<p><a href="https://www.youtube.com/watch?v=0UeooYXCN3g&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=2 ">https://www.youtube.com/watch?v=0UeooYXCN3g&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=2 </a></p>



<p>第四集：和室友Rina一起破解魔法物品，进入废弃的英国地铁站寻找进行过恶魔仪式的教堂地下。</p>



<p><a href="https://www.youtube.com/watch?v=lHv8x_N2W6c&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=1 ">https://www.youtube.com/watch?v=lHv8x_N2W6c&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=1 </a></p>



<p>第五集/大结局（本视频）：解谜地下教堂，发现被恶魔囚禁的灵魂，克服内心的恐惧，击败红衣主教释放灵魂，但最终未能打败大boss，弟弟Adam和恶魔达成协议，游戏全集结束。</p>



<p><a href="https://www.youtube.com/watch?v=HssRQbcsJBI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=5">https://www.youtube.com/watch?v=HssRQbcsJBI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=5</a></p>



<p></p><p>The post <a href="https://mszpro.com/https-mszpro-com-ghost-town-cn-5">【Ghost Town VR鬼镇｜大结局】打败红衣主教，释放无数灵魂，最终并没有打败恶魔大boss？｜2025最新解谜VR游戏大作 by Fireproof Studio｜中文，边讲边玩</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>进入废弃地铁站寻找教堂【Ghost Town VR鬼镇｜第4集】2025最新解谜VR游戏大作【by Fireproof Studio】中文，边讲边玩</title>
		<link>https://mszpro.com/https-mszpro-com-ghost-town-cn-4</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Sat, 10 May 2025 13:13:57 +0000</pubDate>
				<category><![CDATA[Ghost Town 鬼镇 VR游戏 2025]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=803</guid>

					<description><![CDATA[<p>剧情概括 从上一集术士Beaver家中回来后，我们拿到了一个奇妙的由很多小三角形堆积形成的物体。回到公寓后，仿佛来到了另一个维度的空间，灯都焕发着恐怖的红光，进入公寓楼后耳边传来很多声音。但是我们克服恐惧后醒来，发现其实就是一个梦。通过和室友 Rina的研究，和我的解谜，神秘的物体展示出了一个教堂的照片。 我们顺着照片寻找，找到了一个教堂，然而教堂早已被火烧。但是，奇异的是，在教堂的位置建立了地铁，但是地铁因为神秘的原因关闭。结合奇妙物体说出的一句话“Underearth Revelation”我们知道了，其实真正教堂隐藏的秘密在地下，也就是说在修好了但是废弃的地铁站下面。 我前往了地铁站，到达站台位置，发现了一个空中悬浮的鬼魂。。。 中文/边讲边玩 游戏概括 Ghost Town是著名的Fireproof Studio（之前出过最著名的密室逃脱游戏The Room系列）。今年2025年4月推出了最新的VR游戏Ghost Town鬼镇。我们作为一名巫师，解放因为各种缘由纠缠的灵魂。 观看全集 第一集：和弟弟去剧院，弟弟突然间触碰灵魂导致驱魔失败，释放灯塔灵魂，Angela寻找弟弟下落 https://www.youtube.com/watch?v=rI8kKRPUnns&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=4 第二集：用室友Rina的电脑破译Angela手绘标志，找到奇异的Black Doorway魔咒符号。 https://www.youtube.com/watch?v=lwyAwWz0pgI&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=3 第三集：潜入魔法术士豪宅触摸每一件魔法物品，被关进机关盒子，逃出并获得奇怪的物品。 https://www.youtube.com/watch?v=0UeooYXCN3g&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=2 第四集（本视频）：和室友Rina一起破解魔法物品，进入废弃的英国地铁站寻找进行过恶魔仪式的教堂地下。 https://www.youtube.com/watch?v=lHv8x_N2W6c&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=1 第五集/大结局：解谜地下教堂，发现被恶魔囚禁的灵魂，克服内心的恐惧，击败红衣主教释放灵魂，但最终未能打败大boss，弟弟Adam和恶魔达成协议，游戏全集结束。 https://www.youtube.com/watch?v=HssRQbcsJBI&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=5</p>
<p>The post <a href="https://mszpro.com/https-mszpro-com-ghost-town-cn-4">进入废弃地铁站寻找教堂【Ghost Town VR鬼镇｜第4集】2025最新解谜VR游戏大作【by Fireproof Studio】中文，边讲边玩</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">剧情概括</h2>



<p>从上一集术士Beaver家中回来后，我们拿到了一个奇妙的由很多小三角形堆积形成的物体。回到公寓后，仿佛来到了另一个维度的空间，灯都焕发着恐怖的红光，进入公寓楼后耳边传来很多声音。但是我们克服恐惧后醒来，发现其实就是一个梦。通过和室友</p>



<p>Rina的研究，和我的解谜，神秘的物体展示出了一个教堂的照片。 我们顺着照片寻找，找到了一个教堂，然而教堂早已被火烧。但是，奇异的是，在教堂的位置建立了地铁，但是地铁因为神秘的原因关闭。结合奇妙物体说出的一句话“Underearth Revelation”我们知道了，其实真正教堂隐藏的秘密在地下，也就是说在修好了但是废弃的地铁站下面。 </p>



<p>我前往了地铁站，到达站台位置，发现了一个空中悬浮的鬼魂。。。</p>



<h2 class="wp-block-heading">中文/边讲边玩</h2>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe title="进入废弃地铁站寻找教堂【Ghost Town 鬼镇｜第4集】2025最新解谜VR游戏大作【by Fireproof Studio】中文，边讲边玩" width="500" height="281" src="https://www.youtube.com/embed/lHv8x_N2W6c?list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<h2 class="wp-block-heading">游戏概括</h2>



<p>Ghost Town是著名的Fireproof Studio（之前出过最著名的密室逃脱游戏The Room系列）。今年2025年4月推出了最新的VR游戏Ghost Town鬼镇。我们作为一名巫师，解放因为各种缘由纠缠的灵魂。</p>



<h2 class="wp-block-heading">观看全集</h2>



<p>第一集：和弟弟去剧院，弟弟突然间触碰灵魂导致驱魔失败，释放灯塔灵魂，Angela寻找弟弟下落 <a href="https://www.youtube.com/watch?v=rI8kKRPUnns&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=4">https://www.youtube.com/watch?v=rI8kKRPUnns&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=4</a></p>



<p> 第二集：用室友Rina的电脑破译Angela手绘标志，找到奇异的Black Doorway魔咒符号。</p>



<p><a href="https://www.youtube.com/watch?v=lwyAwWz0pgI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=3" title="">https://www.youtube.com/watch?v=lwyAwWz0pgI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=3 </a></p>



<p>第三集：潜入魔法术士豪宅触摸每一件魔法物品，被关进机关盒子，逃出并获得奇怪的物品。</p>



<p><a href="https://www.youtube.com/watch?v=0UeooYXCN3g&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=2 ">https://www.youtube.com/watch?v=0UeooYXCN3g&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=2 </a></p>



<p>第四集（本视频）：和室友Rina一起破解魔法物品，进入废弃的英国地铁站寻找进行过恶魔仪式的教堂地下。</p>



<p><a href="https://www.youtube.com/watch?v=lHv8x_N2W6c&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=1 ">https://www.youtube.com/watch?v=lHv8x_N2W6c&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=1 </a></p>



<p>第五集/大结局：解谜地下教堂，发现被恶魔囚禁的灵魂，克服内心的恐惧，击败红衣主教释放灵魂，但最终未能打败大boss，弟弟Adam和恶魔达成协议，游戏全集结束。</p>



<p><a href="https://www.youtube.com/watch?v=HssRQbcsJBI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=5">https://www.youtube.com/watch?v=HssRQbcsJBI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=5</a></p>



<p></p><p>The post <a href="https://mszpro.com/https-mszpro-com-ghost-town-cn-4">进入废弃地铁站寻找教堂【Ghost Town VR鬼镇｜第4集】2025最新解谜VR游戏大作【by Fireproof Studio】中文，边讲边玩</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>潜入魔法术士豪宅发现秘宝【Ghost Town VR鬼镇｜第3集】2025最新解谜VR游戏大作by Fireproof Studio｜中文，边讲边玩</title>
		<link>https://mszpro.com/https-mszpro-com-ghost-town-cn-3</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Sat, 10 May 2025 13:11:15 +0000</pubDate>
				<category><![CDATA[Ghost Town 鬼镇 VR游戏 2025]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=801</guid>

					<description><![CDATA[<p>剧情概括 • 追随先知 Angela 留下的符号，我们潜入术士 Beaver 的豪宅寻找线索。 • 回忆起一年前，我和失踪的弟弟 Adam 在此偷走恶魔面具的往事。 • Beaver 告诉我们，这面具属于恶魔 Dantalion。佩戴者会执行名为 “黑门 Black Doorway” 的仪式，灵魂将被封印。 • 进入 Nightyberg 留下的魔盒后，我们 解除机关，获得诡异的三角装置，并听到黑暗低语：“加入我们……” • 回到伦敦公寓时，一切被红光与阴影笼罩。 中文/边讲边玩 游戏概括 Ghost Town是著名的Fireproof Studio（之前出过最著名的密室逃脱游戏The Room系列）。今年2025年4月推出了最新的VR游戏Ghost Town鬼镇。我们作为一名巫师，解放因为各种缘由纠缠的灵魂。 观看全集 第一集：和弟弟去剧院，弟弟突然间触碰灵魂导致驱魔失败，释放灯塔灵魂，Angela寻找弟弟下落 https://www.youtube.com/watch?v=rI8kKRPUnns&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=4 第二集：用室友Rina的电脑破译Angela手绘标志，找到奇异的Black Doorway魔咒符号。 https://www.youtube.com/watch?v=lwyAwWz0pgI&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=3 第三集（本视频）：潜入魔法术士豪宅触摸每一件魔法物品，被关进机关盒子，逃出并获得奇怪的物品。 https://www.youtube.com/watch?v=0UeooYXCN3g&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=2 第四集：和室友Rina一起破解魔法物品，进入废弃的英国地铁站寻找进行过恶魔仪式的教堂地下。 https://www.youtube.com/watch?v=lHv8x_N2W6c&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=1 第五集/大结局：解谜地下教堂，发现被恶魔囚禁的灵魂，克服内心的恐惧，击败红衣主教释放灵魂，但最终未能打败大boss，弟弟Adam和恶魔达成协议，游戏全集结束。 https://www.youtube.com/watch?v=HssRQbcsJBI&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=5</p>
<p>The post <a href="https://mszpro.com/https-mszpro-com-ghost-town-cn-3">潜入魔法术士豪宅发现秘宝【Ghost Town VR鬼镇｜第3集】2025最新解谜VR游戏大作by Fireproof Studio｜中文，边讲边玩</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">剧情概括</h2>



<p>• 追随先知 Angela 留下的符号，我们潜入术士 Beaver 的豪宅寻找线索。 </p>



<p>• 回忆起一年前，我和失踪的弟弟 Adam 在此偷走恶魔面具的往事。 </p>



<p>• Beaver 告诉我们，这面具属于恶魔 Dantalion。佩戴者会执行名为 “黑门 Black Doorway” 的仪式，灵魂将被封印。 </p>



<p>• 进入 Nightyberg 留下的魔盒后，我们</p>



<p>解除机关，获得诡异的三角装置，并听到黑暗低语：“加入我们……” • 回到伦敦公寓时，一切被红光与阴影笼罩。</p>



<h2 class="wp-block-heading">中文/边讲边玩</h2>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe title="潜入魔法术士豪宅发现秘宝【Ghost Town 鬼镇｜第3集】2025最新解谜VR游戏大作【by Fireproof Studio】中文，边讲边玩" width="500" height="281" src="https://www.youtube.com/embed/0UeooYXCN3g?list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<h2 class="wp-block-heading">游戏概括</h2>



<p>Ghost Town是著名的Fireproof Studio（之前出过最著名的密室逃脱游戏The Room系列）。今年2025年4月推出了最新的VR游戏Ghost Town鬼镇。我们作为一名巫师，解放因为各种缘由纠缠的灵魂。</p>



<h2 class="wp-block-heading">观看全集</h2>



<p>第一集：和弟弟去剧院，弟弟突然间触碰灵魂导致驱魔失败，释放灯塔灵魂，Angela寻找弟弟下落 <a href="https://www.youtube.com/watch?v=rI8kKRPUnns&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=4">https://www.youtube.com/watch?v=rI8kKRPUnns&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=4</a></p>



<p> 第二集：用室友Rina的电脑破译Angela手绘标志，找到奇异的Black Doorway魔咒符号。</p>



<p><a href="https://www.youtube.com/watch?v=lwyAwWz0pgI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=3" title="">https://www.youtube.com/watch?v=lwyAwWz0pgI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=3 </a></p>



<p>第三集（本视频）：潜入魔法术士豪宅触摸每一件魔法物品，被关进机关盒子，逃出并获得奇怪的物品。</p>



<p><a href="https://www.youtube.com/watch?v=0UeooYXCN3g&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=2 ">https://www.youtube.com/watch?v=0UeooYXCN3g&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=2 </a></p>



<p>第四集：和室友Rina一起破解魔法物品，进入废弃的英国地铁站寻找进行过恶魔仪式的教堂地下。</p>



<p><a href="https://www.youtube.com/watch?v=lHv8x_N2W6c&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=1 ">https://www.youtube.com/watch?v=lHv8x_N2W6c&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=1 </a></p>



<p>第五集/大结局：解谜地下教堂，发现被恶魔囚禁的灵魂，克服内心的恐惧，击败红衣主教释放灵魂，但最终未能打败大boss，弟弟Adam和恶魔达成协议，游戏全集结束。</p>



<p><a href="https://www.youtube.com/watch?v=HssRQbcsJBI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=5">https://www.youtube.com/watch?v=HssRQbcsJBI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=5</a></p><p>The post <a href="https://mszpro.com/https-mszpro-com-ghost-town-cn-3">潜入魔法术士豪宅发现秘宝【Ghost Town VR鬼镇｜第3集】2025最新解谜VR游戏大作by Fireproof Studio｜中文，边讲边玩</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>在灯塔的Angela和我的室友Rina给我线索找到弟弟Adam【Ghost Town VR 鬼镇｜第2集】2025/4月最新恐怖解谜VR游戏大作 by Fireproof Studio｜中文，边讲边玩</title>
		<link>https://mszpro.com/ghost-town-cn-2</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Sat, 10 May 2025 13:07:48 +0000</pubDate>
				<category><![CDATA[Ghost Town 鬼镇 VR游戏 2025]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=799</guid>

					<description><![CDATA[<p>剧情概括 继上一集，我们释放了灯塔管理人的灵魂，并换好灯塔的灯泡后，我们找到Angela询问我失踪的弟弟的信息。Angela通过我弟弟随身携带的物品建立了和他的联系。我看到了我的弟弟Adam，他带着一副奇怪的面具。但是突然，进入了地狱般的环境。Angela被恶魔操作说出：黑色的大门已经打开。。。随后Angela一直在乱语，直到我把她叫醒，她把她手绘写满符号的纸给我，让我研究。 我回到了我的公寓，通过我室友的机器，研究出了隐藏的符号。这个符号显示的资料里面有一张照片，是一个带着面具的人坐在椅子上，和我弟弟带的面具一样。但是这个资料其他部分是保密的，我必须前往Weaver家中探询线索。 中文/边讲边玩 游戏概括 Ghost Town是著名的Fireproof Studio（之前出过最著名的密室逃脱游戏The Room系列）。今年2025年4月推出了最新的VR游戏Ghost Town鬼镇。我们作为一名巫师，解放因为各种缘由纠缠的灵魂。 观看全集 第一集：和弟弟去剧院，弟弟突然间触碰灵魂导致驱魔失败，释放灯塔灵魂，Angela寻找弟弟下落 https://www.youtube.com/watch?v=rI8kKRPUnns&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=4 第二集（本视频）：用室友Rina的电脑破译Angela手绘标志，找到奇异的Black Doorway魔咒符号。 https://www.youtube.com/watch?v=lwyAwWz0pgI&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=3 第三集：潜入魔法术士豪宅触摸每一件魔法物品，被关进机关盒子，逃出并获得奇怪的物品。 https://www.youtube.com/watch?v=0UeooYXCN3g&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=2 第四集：和室友Rina一起破解魔法物品，进入废弃的英国地铁站寻找进行过恶魔仪式的教堂地下。 https://www.youtube.com/watch?v=lHv8x_N2W6c&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=1 第五集/大结局：解谜地下教堂，发现被恶魔囚禁的灵魂，克服内心的恐惧，击败红衣主教释放灵魂，但最终未能打败大boss，弟弟Adam和恶魔达成协议，游戏全集结束。 https://www.youtube.com/watch?v=HssRQbcsJBI&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=5</p>
<p>The post <a href="https://mszpro.com/ghost-town-cn-2">在灯塔的Angela和我的室友Rina给我线索找到弟弟Adam【Ghost Town VR 鬼镇｜第2集】2025/4月最新恐怖解谜VR游戏大作 by Fireproof Studio｜中文，边讲边玩</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">剧情概括</h2>



<p>继上一集，我们释放了灯塔管理人的灵魂，并换好灯塔的灯泡后，我们找到Angela询问我失踪的弟弟的信息。Angela通过我弟弟随身携带的物品建立了和他的联系。我看到了我的弟弟Adam，他带着一副奇怪的面具。但是突然，进入了地狱般的环境。Angela被恶魔操作说出：黑色的大门已经打开。。。随后Angela一直在乱语，直到我把她叫醒，她把她手绘写满符号的纸给我，让我研究。</p>



<p> 我回到了我的公寓，通过我室友的机器，研究出了隐藏的符号。这个符号显示的资料里面有一张照片，是一个带着面具的人坐在椅子上，和我弟弟带的面具一样。但是这个资料其他部分是保密的，我必须前往Weaver家中探询线索。</p>



<h2 class="wp-block-heading">中文/边讲边玩</h2>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="Angela和我的室友给我线索找到弟弟Adam【Ghost Town 鬼镇｜第2集】2025/4月最新恐怖解谜VR游戏大作【by Fireproof Studio】中文，边讲边玩" width="500" height="281" src="https://www.youtube.com/embed/lwyAwWz0pgI?list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<h2 class="wp-block-heading">游戏概括</h2>



<p>Ghost Town是著名的Fireproof Studio（之前出过最著名的密室逃脱游戏The Room系列）。今年2025年4月推出了最新的VR游戏Ghost Town鬼镇。我们作为一名巫师，解放因为各种缘由纠缠的灵魂。</p>



<h2 class="wp-block-heading">观看全集</h2>



<p>第一集：和弟弟去剧院，弟弟突然间触碰灵魂导致驱魔失败，释放灯塔灵魂，Angela寻找弟弟下落 <a href="https://www.youtube.com/watch?v=rI8kKRPUnns&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=4">https://www.youtube.com/watch?v=rI8kKRPUnns&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=4</a></p>



<p>第二集（本视频）：用室友Rina的电脑破译Angela手绘标志，找到奇异的Black Doorway魔咒符号。</p>



<p><a href="https://www.youtube.com/watch?v=lwyAwWz0pgI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=3" title="">https://www.youtube.com/watch?v=lwyAwWz0pgI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=3 </a></p>



<p>第三集：潜入魔法术士豪宅触摸每一件魔法物品，被关进机关盒子，逃出并获得奇怪的物品。</p>



<p><a href="https://www.youtube.com/watch?v=0UeooYXCN3g&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=2 ">https://www.youtube.com/watch?v=0UeooYXCN3g&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=2 </a></p>



<p>第四集：和室友Rina一起破解魔法物品，进入废弃的英国地铁站寻找进行过恶魔仪式的教堂地下。</p>



<p><a href="https://www.youtube.com/watch?v=lHv8x_N2W6c&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=1 ">https://www.youtube.com/watch?v=lHv8x_N2W6c&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=1 </a></p>



<p>第五集/大结局：解谜地下教堂，发现被恶魔囚禁的灵魂，克服内心的恐惧，击败红衣主教释放灵魂，但最终未能打败大boss，弟弟Adam和恶魔达成协议，游戏全集结束。</p>



<p><a href="https://www.youtube.com/watch?v=HssRQbcsJBI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=5">https://www.youtube.com/watch?v=HssRQbcsJBI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=5</a></p>



<p></p><p>The post <a href="https://mszpro.com/ghost-town-cn-2">在灯塔的Angela和我的室友Rina给我线索找到弟弟Adam【Ghost Town VR 鬼镇｜第2集】2025/4月最新恐怖解谜VR游戏大作 by Fireproof Studio｜中文，边讲边玩</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>化身巫师解放幽魂【Ghost Town 鬼镇｜第1集】2025最新解谜VR游戏【by Fireproof Studio】中文，边讲边玩</title>
		<link>https://mszpro.com/ghost-town-cn-1</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Sat, 10 May 2025 12:59:45 +0000</pubDate>
				<category><![CDATA[Ghost Town 鬼镇 VR游戏 2025]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=795</guid>

					<description><![CDATA[<p>剧情概括 四年前，主角Edith（我）和弟弟Adam闯入一座古老剧院，尝试解救女演员凯瑟琳·伍兹的灵魂——她因道具匕首失灵，当场丧命，误以为自己被新人顶替。当我的咒语快要成功时，弟弟Adam突如其来的打断让人寒毛直立，黑暗势力似乎盯上了他…… 如今四年过去，Adam离奇失踪。我追踪线索来到塔纳岩岛，与通灵师Angela在老灯塔重聚。刚进门灯泡就熄灭，我们本想修好灯塔，却遇到醉酒误事、导致船只失事的前灯塔看守怨灵。要想让他安息，必须破解电路谜题、收集三件圣物！ 中文/边讲边玩 图片解释 我和我的弟弟Adam去剧院解决关于演员Catherine Woods的灵魂问题。Catherine Woods在她最重要的表演时被道具砸到去世。 所以Catherine的灵魂一只在剧院里呆着，所以我们来解放这个灵魂。 我们来到剧院，调整聚光灯（输出功率匹配360w），让Catherine的鬼魂在此出现在台上 我在尝试释放Catherine的灵魂，但是这时候我的弟弟Adam嫉妒我碰到了鬼魂，说自己也能看到鬼魂。导致驱魔失败。 三年后我的弟弟Adam失踪，我去一个岛上去问Angela先知关于我弟弟的信息。 我们先要去灯塔中解决灯塔的灵魂。灯塔管理员在世时因为喝醉，没有及时的打开灯塔，导致船只闯到，船员死亡，灯塔管理员因为自责选择结束自己的生命。所以我们来释放灯塔管理员的灵魂。 其中需要完成一些电路puzzle，主要就是经过红色晶石电流会削弱，所以尽量不要经过红色晶石。手直接指着从电源源头，然后往灯的电路方向走，然后看着线路进入分线盒，调整里面的金属片，链接到没有红色的线路即可。 然后，我们上到楼顶，到另外一边有一幅油画，画着大章鱼克苏鲁，船，月亮，和灯塔，记住他们的顺序再拼出来 然后成功释放灯塔管理者灵魂 我们下一集继续找Angela问我弟弟的下落。 游戏概括 Ghost Town是著名的Fireproof Studio（之前出过最著名的密室逃脱游戏The Room系列）。今年2025年4月推出了最新的VR游戏Ghost Town鬼镇。我们作为一名巫师，解放因为各种缘由纠缠的灵魂。 观看全集 第一集（本视频）：和弟弟去剧院，弟弟突然间触碰灵魂导致驱魔失败，释放灯塔灵魂，Angela寻找弟弟下落 https://www.youtube.com/watch?v=rI8kKRPUnns&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=4 第二集：用室友Rina的电脑破译Angela手绘标志，找到奇异的Black Doorway魔咒符号。 https://www.youtube.com/watch?v=lwyAwWz0pgI&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=3 第三集：潜入魔法术士豪宅触摸每一件魔法物品，被关进机关盒子，逃出并获得奇怪的物品。 https://www.youtube.com/watch?v=0UeooYXCN3g&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=2 第四集：和室友Rina一起破解魔法物品，进入废弃的英国地铁站寻找进行过恶魔仪式的教堂地下。 https://www.youtube.com/watch?v=lHv8x_N2W6c&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=1 第五集/大结局：解谜地下教堂，发现被恶魔囚禁的灵魂，克服内心的恐惧，击败红衣主教释放灵魂，但最终未能打败大boss，弟弟Adam和恶魔达成协议，游戏全集结束。 https://www.youtube.com/watch?v=HssRQbcsJBI&#38;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&#38;index=5</p>
<p>The post <a href="https://mszpro.com/ghost-town-cn-1">化身巫师解放幽魂【Ghost Town 鬼镇｜第1集】2025最新解谜VR游戏【by Fireproof Studio】中文，边讲边玩</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">剧情概括</h2>



<p>四年前，主角Edith（我）和弟弟Adam闯入一座古老剧院，尝试解救女演员凯瑟琳·伍兹的灵魂——她因道具匕首失灵，当场丧命，误以为自己被新人顶替。当我的咒语快要成功时，弟弟Adam突如其来的打断让人寒毛直立，黑暗势力似乎盯上了他…… 如今四年过去，Adam离奇失踪。我追踪线索来到塔纳岩岛，与通灵师Angela在老灯塔重聚。刚进门灯泡就熄灭，我们本想修好灯塔，却遇到醉酒误事、导致船只失事的前灯塔看守怨灵。要想让他安息，必须破解电路谜题、收集三件圣物！ </p>



<h2 class="wp-block-heading">中文/边讲边玩</h2>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="化身巫师解放幽魂！【Ghost Town 鬼镇｜第1集】2025/4月最新恐怖｜解谜VR游戏大作【by Fireproof Studio】中文，边讲边玩" width="500" height="281" src="https://www.youtube.com/embed/rI8kKRPUnns?list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<h2 class="wp-block-heading">图片解释</h2>



<p>我和我的弟弟Adam去剧院解决关于演员Catherine Woods的灵魂问题。Catherine Woods在她最重要的表演时被道具砸到去世。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="603" src="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-21.55.30-1024x603.jpg" alt="" class="wp-image-817" srcset="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-21.55.30-300x177.jpg 300w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-21.55.30-1024x603.jpg 1024w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-21.55.30-768x453.jpg 768w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-21.55.30-1536x905.jpg 1536w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-21.55.30-2048x1207.jpg 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Screenshot</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="574" src="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.24.53-1-1024x574.png" alt="" class="wp-image-818" srcset="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.24.53-1-300x168.png 300w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.24.53-1-1024x574.png 1024w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.24.53-1-768x431.png 768w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.24.53-1-1536x862.png 1536w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.24.53-1-2048x1149.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>所以Catherine的灵魂一只在剧院里呆着，所以我们来解放这个灵魂。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="575" src="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.02-1-1024x575.png" alt="" class="wp-image-819" srcset="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.02-1-300x168.png 300w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.02-1-1024x575.png 1024w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.02-1-768x431.png 768w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.02-1-1536x862.png 1536w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.02-1-2048x1149.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>我们来到剧院，调整聚光灯（输出功率匹配360w），让Catherine的鬼魂在此出现在台上</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="572" src="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.14-1024x572.png" alt="" class="wp-image-820" srcset="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.14-300x167.png 300w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.14-1024x572.png 1024w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.14-768x429.png 768w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.14-1536x857.png 1536w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.14-2048x1143.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>我在尝试释放Catherine的灵魂，但是这时候我的弟弟Adam嫉妒我碰到了鬼魂，说自己也能看到鬼魂。导致驱魔失败。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="570" src="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.39-1024x570.jpg" alt="" class="wp-image-821" srcset="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.39-300x167.jpg 300w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.39-1024x570.jpg 1024w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.39-768x427.jpg 768w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.39-1536x855.jpg 1536w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.39-2048x1140.jpg 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Screenshot</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="601" src="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.31.54-1024x601.jpg" alt="" class="wp-image-822" srcset="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.31.54-300x176.jpg 300w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.31.54-1024x601.jpg 1024w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.31.54-768x451.jpg 768w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.31.54-1536x902.jpg 1536w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.31.54-2048x1203.jpg 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Screenshot</figcaption></figure>



<p>三年后我的弟弟Adam失踪，我去一个岛上去问Angela先知关于我弟弟的信息。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="574" src="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.57-1024x574.jpg" alt="" class="wp-image-823" srcset="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.57-300x168.jpg 300w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.57-1024x574.jpg 1024w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.57-768x430.jpg 768w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.57-1536x861.jpg 1536w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.25.57-2048x1148.jpg 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Screenshot</figcaption></figure>



<p>我们先要去灯塔中解决灯塔的灵魂。灯塔管理员在世时因为喝醉，没有及时的打开灯塔，导致船只闯到，船员死亡，灯塔管理员因为自责选择结束自己的生命。所以我们来释放灯塔管理员的灵魂。</p>



<p>其中需要完成一些电路puzzle，主要就是经过红色晶石电流会削弱，所以尽量不要经过红色晶石。手直接指着从电源源头，然后往灯的电路方向走，然后看着线路进入分线盒，调整里面的金属片，链接到没有红色的线路即可。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="571" src="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.26.29-1024x571.jpg" alt="" class="wp-image-824" srcset="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.26.29-300x167.jpg 300w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.26.29-1024x571.jpg 1024w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.26.29-768x428.jpg 768w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.26.29-1536x856.jpg 1536w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.26.29-2048x1142.jpg 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Screenshot</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="573" src="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.26.50-1024x573.jpg" alt="" class="wp-image-825" srcset="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.26.50-300x168.jpg 300w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.26.50-1024x573.jpg 1024w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.26.50-768x430.jpg 768w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.26.50-1536x860.jpg 1536w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.26.50-2048x1147.jpg 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Screenshot</figcaption></figure>



<p>然后，我们上到楼顶，到另外一边有一幅油画，画着大章鱼克苏鲁，船，月亮，和灯塔，记住他们的顺序再拼出来</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="576" src="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.27.11-1024x576.jpg" alt="" class="wp-image-826" srcset="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.27.11-300x169.jpg 300w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.27.11-1024x576.jpg 1024w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.27.11-768x432.jpg 768w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.27.11-1536x864.jpg 1536w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.27.11-2048x1152.jpg 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Screenshot</figcaption></figure>



<p>然后成功释放灯塔管理者灵魂</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="572" src="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.27.25-1024x572.jpg" alt="" class="wp-image-827" srcset="https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.27.25-300x168.jpg 300w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.27.25-1024x572.jpg 1024w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.27.25-768x429.jpg 768w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.27.25-1536x858.jpg 1536w, https://static-assets.mszpro.com/2025/05/Screenshot-2025-05-10-at-22.27.25-2048x1144.jpg 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Screenshot</figcaption></figure>



<p>我们下一集继续找Angela问我弟弟的下落。</p>



<h2 class="wp-block-heading">游戏概括</h2>



<p>Ghost Town是著名的Fireproof Studio（之前出过最著名的密室逃脱游戏The Room系列）。今年2025年4月推出了最新的VR游戏Ghost Town鬼镇。我们作为一名巫师，解放因为各种缘由纠缠的灵魂。</p>



<h2 class="wp-block-heading">观看全集</h2>



<p>第一集（本视频）：和弟弟去剧院，弟弟突然间触碰灵魂导致驱魔失败，释放灯塔灵魂，Angela寻找弟弟下落 <a href="https://www.youtube.com/watch?v=rI8kKRPUnns&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=4">https://www.youtube.com/watch?v=rI8kKRPUnns&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=4</a></p>



<p>第二集：用室友Rina的电脑破译Angela手绘标志，找到奇异的Black Doorway魔咒符号。</p>



<p><a href="https://www.youtube.com/watch?v=lwyAwWz0pgI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=3" title="">https://www.youtube.com/watch?v=lwyAwWz0pgI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=3 </a></p>



<p>第三集：潜入魔法术士豪宅触摸每一件魔法物品，被关进机关盒子，逃出并获得奇怪的物品。</p>



<p><a href="https://www.youtube.com/watch?v=0UeooYXCN3g&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=2 ">https://www.youtube.com/watch?v=0UeooYXCN3g&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=2 </a></p>



<p>第四集：和室友Rina一起破解魔法物品，进入废弃的英国地铁站寻找进行过恶魔仪式的教堂地下。</p>



<p><a href="https://www.youtube.com/watch?v=lHv8x_N2W6c&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=1 ">https://www.youtube.com/watch?v=lHv8x_N2W6c&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=1 </a></p>



<p>第五集/大结局：解谜地下教堂，发现被恶魔囚禁的灵魂，克服内心的恐惧，击败红衣主教释放灵魂，但最终未能打败大boss，弟弟Adam和恶魔达成协议，游戏全集结束。</p>



<p><a href="https://www.youtube.com/watch?v=HssRQbcsJBI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=5">https://www.youtube.com/watch?v=HssRQbcsJBI&amp;list=PLulQbkfFGIGhqldFDEDUMApBhtdOpJRak&amp;index=5</a></p>



<p></p><p>The post <a href="https://mszpro.com/ghost-town-cn-1">化身巫师解放幽魂【Ghost Town 鬼镇｜第1集】2025最新解谜VR游戏【by Fireproof Studio】中文，边讲边玩</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Tokyo Disney 2-day travel guide &#8211; DisneySea &#038; Land 2025 February &#8211; 15 Attractions</title>
		<link>https://mszpro.com/tokyo-disney-2025</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Mon, 03 Feb 2025 05:02:16 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=631</guid>

					<description><![CDATA[<p>I went to Tokyo Disney for 2 days, which includes the Tokyo Disney Sea on day one and Tokyo Island on day 2. I played a total of 15 attractions &#8211; 7 at DisneySea and 8 at the Disneyland . For me, Disney is like a recharge station for my spirit, it fills you with [&#8230;]</p>
<p>The post <a href="https://mszpro.com/tokyo-disney-2025">Tokyo Disney 2-day travel guide – DisneySea & Land 2025 February – 15 Attractions</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>I went to Tokyo Disney for 2 days, which includes the Tokyo Disney Sea on day one and Tokyo Island on day 2. I played a total of 15 attractions &#8211; 7 at DisneySea and 8 at the Disneyland .</p>



<p>For me, Disney is like a recharge station for my spirit, it fills you with imaginations and brightness to your heart. I hope you feel the same!</p>



<p>If you arrived to Tokyo via Shinkansen, you can take the Keiyo train line toward the Maihama station.</p>



<iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d921.0466522201375!2d139.88326941502407!3d35.6360212632416!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x60187d118dd7c153%3A0x11bed1a01ae12fc3!2sMaihama%20Station!5e0!3m2!1sen!2sjp!4v1738496101910!5m2!1sen!2sjp" width="600" height="450" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>



<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="586" height="1024" src="https://static-assets.mszpro.com/2025/02/IMG_8AE8BB0A3E45-1-586x1024.jpeg" alt="" class="wp-image-632" srcset="https://static-assets.mszpro.com/2025/02/IMG_8AE8BB0A3E45-1-172x300.jpeg 172w, https://static-assets.mszpro.com/2025/02/IMG_8AE8BB0A3E45-1-586x1024.jpeg 586w, https://static-assets.mszpro.com/2025/02/IMG_8AE8BB0A3E45-1-768x1342.jpeg 768w, https://static-assets.mszpro.com/2025/02/IMG_8AE8BB0A3E45-1-879x1536.jpeg 879w, https://static-assets.mszpro.com/2025/02/IMG_8AE8BB0A3E45-1-1172x2048.jpeg 1172w, https://static-assets.mszpro.com/2025/02/IMG_8AE8BB0A3E45-1.jpeg 1320w" sizes="auto, (max-width: 586px) 100vw, 586px" /></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<p>The Disney Resort Line has 4 stations.</p>



<p>When you get off from the JR Keiyo line Maihama station (京葉線 舞浜駅), you are at the Resort Gateway Station.</p>



<p>The train goes in a circle, from Resort Gateway Station (JR Train line) to Tokyo Disneyland Staton, to Bayside Station (hotels), and to the Tokyo DisneySea Station (entrance to Disney Sea).</p>
</div>
</div>



<p>You can use Suica or Icoca for the train, however, if you are staying for more than a day, you can purchase a 2-day pass:</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-full"><img decoding="async" src="https://static-assets.mszpro.com/2025/02/IMG_4663.heic" alt="" class="wp-image-634"/></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<p>A 2-day pass allows you to take trains of the Tokyo Disney Resort line at any time.</p>



<p>You can also get a character image there.</p>



<p>There is a key: different ticketing machines issue a different character design. Please pay attention to the image shown on machine screen before making the purchase.</p>
</div>
</div>



<p>You should 1000% download and use the Disney app, since you will be able to get Standby pass and Disney Premier Access pass to access attractions and shows quickly, and to use mobile orders at restaurants and skip the line (believe me the lines are sometimes long). </p>



<p>I was able to play more than 10 attractions within 2 days by using the Disney Premier Access (DPA), which is a few taps away in the app.</p>



<p>Here is what I played at the park:</p>



<h2 class="wp-block-heading">Day one (7 attractions) at DisneySea</h2>



<p>Arrived at 1PM at Tokyo Disney Sea:</p>



<ul class="wp-block-list">
<li>20,000 Leagues Under the Sea</li>
</ul>



<p>Recommendation level: ⭐️⭐️⭐️🌟 Cool</p>



<p>You will go into a submarine and it will go under water to explore. </p>



<p>I did not have to purchase a DPA and the wait is only 10 minutes.</p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="600" height="1024" data-id="636" src="https://static-assets.mszpro.com/2025/02/IMG_531608F7F9F8-1-600x1024.jpeg" alt="" class="wp-image-636" srcset="https://static-assets.mszpro.com/2025/02/IMG_531608F7F9F8-1-176x300.jpeg 176w, https://static-assets.mszpro.com/2025/02/IMG_531608F7F9F8-1-600x1024.jpeg 600w, https://static-assets.mszpro.com/2025/02/IMG_531608F7F9F8-1-768x1311.jpeg 768w, https://static-assets.mszpro.com/2025/02/IMG_531608F7F9F8-1-900x1536.jpeg 900w, https://static-assets.mszpro.com/2025/02/IMG_531608F7F9F8-1-1199x2048.jpeg 1199w, https://static-assets.mszpro.com/2025/02/IMG_531608F7F9F8-1.jpeg 1320w" sizes="auto, (max-width: 600px) 100vw, 600px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="583" height="1024" data-id="635" src="https://static-assets.mszpro.com/2025/02/View-recent-photos-583x1024.jpeg" alt="" class="wp-image-635" srcset="https://static-assets.mszpro.com/2025/02/View-recent-photos-171x300.jpeg 171w, https://static-assets.mszpro.com/2025/02/View-recent-photos-583x1024.jpeg 583w, https://static-assets.mszpro.com/2025/02/View-recent-photos-768x1348.jpeg 768w, https://static-assets.mszpro.com/2025/02/View-recent-photos-875x1536.jpeg 875w, https://static-assets.mszpro.com/2025/02/View-recent-photos-1167x2048.jpeg 1167w, https://static-assets.mszpro.com/2025/02/View-recent-photos.jpeg 1320w" sizes="auto, (max-width: 583px) 100vw, 583px" /></figure>
</figure>



<ul class="wp-block-list">
<li>Journey to the Center of the Earth (DPA)</li>
</ul>



<p>Recommendation level: ⭐️⭐️⭐️⭐️🌟 MUST</p>



<p>This is super exciting! You will take an excavator and explore within a volcano. At the end of the ride, it will go on roller coaster mode!</p>



<p>Here is the official Disney video:</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="【公式】センター・オブ・ジ・アース / Journey to the Center of the Earth | 東京ディズニーシー/Tokyo DisneySea" width="500" height="281" src="https://www.youtube.com/embed/KjQOMyit-sM?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<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="581" height="1024" src="https://static-assets.mszpro.com/2025/02/View-recent-photos-2-581x1024.jpeg" alt="" class="wp-image-637" srcset="https://static-assets.mszpro.com/2025/02/View-recent-photos-2-170x300.jpeg 170w, https://static-assets.mszpro.com/2025/02/View-recent-photos-2-581x1024.jpeg 581w, https://static-assets.mszpro.com/2025/02/View-recent-photos-2-768x1353.jpeg 768w, https://static-assets.mszpro.com/2025/02/View-recent-photos-2-872x1536.jpeg 872w, https://static-assets.mszpro.com/2025/02/View-recent-photos-2-1163x2048.jpeg 1163w, https://static-assets.mszpro.com/2025/02/View-recent-photos-2.jpeg 1320w" sizes="auto, (max-width: 581px) 100vw, 581px" /></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<p>You also get to see captain Nemo&#8217;s lab before getting on the ride.</p>
</div>
</div>



<p>The wait for this is too long! But I purchased the Disney Premier Access within the app and got on the ride within less than 10 minutes.</p>



<p>Here is how to purchase it, it is around $10:</p>



<ul class="wp-block-list">
<li>Register your ticket ahead of time in the Tokyo Disney Resort app</li>



<li>Use the QR code within the app to enter the park</li>



<li>Make sure the app has GPS access</li>



<li>Tap the stars icon at the bottom drawer menu</li>



<li>Tap Disney Premier Access and continue from there</li>



<li>Note that after purchasing one of the DPA, you can only purchase the next one either after you complete the current one, or after an hour.</li>
</ul>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="【公式】ディズニー・プレミアアクセス / Disney Premier Access | 東京ディズニーリゾート/TokyoDisneyResort" width="500" height="281" src="https://www.youtube.com/embed/6f_JV7EsKs4?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<p>Then, after this ride, I went for the relaxing route of taking a boat in Venice:</p>



<ul class="wp-block-list">
<li>Venetian Gondolas</li>
</ul>



<p>You can teleport to Venice quickly and enjoy the boat ride. Don&#8217;t forget to say Ciao when you pass another boat!</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="【公式】ヴェネツィアン・ゴンドラ / Venetian Gondolas | 東京ディズニーシー/Tokyo DisneySea" width="500" height="281" src="https://www.youtube.com/embed/YrXPuDQBKQQ?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<ul class="wp-block-list">
<li>Checked out the US Steamship</li>
</ul>



<p>There is a large ship with multiple decks. You can board it and feel free to check it up:</p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-2 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img decoding="async" data-id="641" src="https://static-assets.mszpro.com/2025/02/IMG_4751.heic" alt="" class="wp-image-641"/></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="582" height="1024" data-id="639" src="https://static-assets.mszpro.com/2025/02/View-recent-photos-3-582x1024.jpeg" alt="" class="wp-image-639" srcset="https://static-assets.mszpro.com/2025/02/View-recent-photos-3-170x300.jpeg 170w, https://static-assets.mszpro.com/2025/02/View-recent-photos-3-582x1024.jpeg 582w, https://static-assets.mszpro.com/2025/02/View-recent-photos-3-768x1352.jpeg 768w, https://static-assets.mszpro.com/2025/02/View-recent-photos-3-873x1536.jpeg 873w, https://static-assets.mszpro.com/2025/02/View-recent-photos-3-1164x2048.jpeg 1164w, https://static-assets.mszpro.com/2025/02/View-recent-photos-3.jpeg 1320w" sizes="auto, (max-width: 582px) 100vw, 582px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="586" height="1024" data-id="640" src="https://static-assets.mszpro.com/2025/02/View-recent-photos-4-586x1024.jpeg" alt="" class="wp-image-640" srcset="https://static-assets.mszpro.com/2025/02/View-recent-photos-4-172x300.jpeg 172w, https://static-assets.mszpro.com/2025/02/View-recent-photos-4-586x1024.jpeg 586w, https://static-assets.mszpro.com/2025/02/View-recent-photos-4-768x1342.jpeg 768w, https://static-assets.mszpro.com/2025/02/View-recent-photos-4-879x1536.jpeg 879w, https://static-assets.mszpro.com/2025/02/View-recent-photos-4-1172x2048.jpeg 1172w, https://static-assets.mszpro.com/2025/02/View-recent-photos-4.jpeg 1320w" sizes="auto, (max-width: 586px) 100vw, 586px" /></figure>



<figure class="wp-block-image size-large"><img decoding="async" data-id="646" src="https://static-assets.mszpro.com/2025/02/IMG_4795.heic" alt="" class="wp-image-646"/></figure>



<figure class="wp-block-image size-large"><img decoding="async" data-id="647" src="https://static-assets.mszpro.com/2025/02/IMG_4796.heic" alt="" class="wp-image-647"/></figure>
</figure>



<p>Pro tip: the ship is much more beautiful at night! Don&#8217;t forget to take a picture!</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="778" height="1024" src="https://static-assets.mszpro.com/2025/02/View-recent-photos-5-778x1024.jpeg" alt="" class="wp-image-642" srcset="https://static-assets.mszpro.com/2025/02/View-recent-photos-5-228x300.jpeg 228w, https://static-assets.mszpro.com/2025/02/View-recent-photos-5-778x1024.jpeg 778w, https://static-assets.mszpro.com/2025/02/View-recent-photos-5-768x1011.jpeg 768w, https://static-assets.mszpro.com/2025/02/View-recent-photos-5-1167x1536.jpeg 1167w, https://static-assets.mszpro.com/2025/02/View-recent-photos-5.jpeg 1320w" sizes="auto, (max-width: 778px) 100vw, 778px" /></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<p>You can see the Disney Resort Line train from on the deck of the boat</p>
</div>
</div>



<ul class="wp-block-list">
<li>Mickey Mouse greetings</li>
</ul>



<p>Then, I went to greet Mickey Mouse who was being a &#8220;tomb raider&#8221; in the<strong> Lost River Delta</strong> region.</p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-3 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img decoding="async" data-id="643" src="https://static-assets.mszpro.com/2025/02/IMG_4756.heic" alt="" class="wp-image-643"/></figure>



<figure class="wp-block-image size-large"><img decoding="async" data-id="644" src="https://static-assets.mszpro.com/2025/02/IMG_4757.heic" alt="" class="wp-image-644"/></figure>



<figure class="wp-block-image size-large"><img decoding="async" data-id="645" src="https://static-assets.mszpro.com/2025/02/IMG_4758.heic" alt="" class="wp-image-645"/></figure>
</figure>



<p>You can give your phone to a crew and they will help you take a picture with Mickey Mouse! Don&#8217;t forget to high-five and smile!</p>



<ul class="wp-block-list">
<li>The new area! Fantasy Springs!</li>
</ul>



<p>Fantasy Springs is a new area opened! There is a beautiful lantern ride (but I did not get the chance to try it 😭 I will try it next time)</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="【公式】ファンタジースプリングス“ラプンツェルの森” | 東京ディズニ－シー/Tokyo DisneySea" width="500" height="281" src="https://www.youtube.com/embed/oV7ubVi05Fs?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<ul class="wp-block-list">
<li>Peter Pan&#8217;s Never Land Adventure</li>
</ul>



<p>I did use the DPA to access the Peter Pan&#8217;s Never Land Adventure! It is amazing! The animation and the effects are beyond my imagination. You feel like you are flying with the characters in the infinite sky!</p>



<p>Recommendation level: ⭐️⭐️⭐️⭐️🌟 MUST</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="【公式】ピーターパンのネバーランドアドベンチャー / Peter Pan&#039;s Never Land Adventure | 東京ディズニーシー/Tokyo DisneySea" width="500" height="281" src="https://www.youtube.com/embed/W_Sh7-_zD38?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<p>Day one was very very exciting! DisneySea is far beyond my imagination! DisneySea feels like a very new park (and indeed it is, with the new Fantasy Springs section)! It has a great layout, and for me, I went from the entrance on the south and played by going all the way North. Until I reach Fantasy Springs (which is on the north side, near the Bayside Disney Resort line station).</p>



<p>The cast members are very very friendly! They are so nice that I remember some of their names and submitted a positive feedback to the DisneySea.</p>



<h2 class="wp-block-heading">Day Two. Disneyland (8 attractions)</h2>



<ul class="wp-block-list">
<li>Big Thunder Mountain</li>
</ul>



<p>Recommendation level: ⭐️⭐️⭐️🌟 Try it out! The wait is not too long! And it is very exciting, with a photo taken when you slide down to the water.</p>



<p>This is a very exciting small roller coaster, which starts with a trip similar to the This is a Small world, but in the close of the ride, you will go down a large slide into the water! It also takes a picture for you!</p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-4 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="575" data-id="650" src="https://static-assets.mszpro.com/2025/02/IMG_13C9667CD0AD-1-1024x575.jpeg" alt="" class="wp-image-650" srcset="https://static-assets.mszpro.com/2025/02/IMG_13C9667CD0AD-1-300x168.jpeg 300w, https://static-assets.mszpro.com/2025/02/IMG_13C9667CD0AD-1-1024x575.jpeg 1024w, https://static-assets.mszpro.com/2025/02/IMG_13C9667CD0AD-1-768x431.jpeg 768w, https://static-assets.mszpro.com/2025/02/IMG_13C9667CD0AD-1.jpeg 1320w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img decoding="async" data-id="648" src="https://static-assets.mszpro.com/2025/02/IMG_4860.heic" alt="" class="wp-image-648"/></figure>



<figure class="wp-block-image size-large"><img decoding="async" data-id="651" src="https://static-assets.mszpro.com/2025/02/IMG_4864.heic" alt="" class="wp-image-651"/></figure>



<figure class="wp-block-image size-large"><img decoding="async" data-id="652" src="https://static-assets.mszpro.com/2025/02/IMG_4870.heic" alt="" class="wp-image-652"/></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="562" data-id="649" src="https://static-assets.mszpro.com/2025/02/IMG_B6646AD9FD04-1-1024x562.jpeg" alt="" class="wp-image-649" srcset="https://static-assets.mszpro.com/2025/02/IMG_B6646AD9FD04-1-300x165.jpeg 300w, https://static-assets.mszpro.com/2025/02/IMG_B6646AD9FD04-1-1024x562.jpeg 1024w, https://static-assets.mszpro.com/2025/02/IMG_B6646AD9FD04-1-768x421.jpeg 768w, https://static-assets.mszpro.com/2025/02/IMG_B6646AD9FD04-1.jpeg 1320w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="578" height="1024" data-id="653" src="https://static-assets.mszpro.com/2025/02/IMG_CE0E53721C7F-1-578x1024.jpeg" alt="" class="wp-image-653" srcset="https://static-assets.mszpro.com/2025/02/IMG_CE0E53721C7F-1-169x300.jpeg 169w, https://static-assets.mszpro.com/2025/02/IMG_CE0E53721C7F-1-578x1024.jpeg 578w, https://static-assets.mszpro.com/2025/02/IMG_CE0E53721C7F-1-768x1360.jpeg 768w, https://static-assets.mszpro.com/2025/02/IMG_CE0E53721C7F-1-868x1536.jpeg 868w, https://static-assets.mszpro.com/2025/02/IMG_CE0E53721C7F-1-1157x2048.jpeg 1157w, https://static-assets.mszpro.com/2025/02/IMG_CE0E53721C7F-1.jpeg 1320w" sizes="auto, (max-width: 578px) 100vw, 578px" /></figure>
</figure>



<p>Did you see the boat on top of the slide in the last picture! Yes! That is the most thrilling &amp; exciting part of this ride!</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="【公式】ビッグサンダー・マウンテン / Big Thunder Mountain | 東京ディズニーランド/Tokyo Disneyland" width="500" height="281" src="https://www.youtube.com/embed/FDcY5m7Pc4k?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<p>Also, I only had to wait for about 10 minutes, and did not use a DPA here.</p>



<ul class="wp-block-list">
<li>Enchanted Tale of Beauty and the Beast</li>
</ul>



<p>Recommendation level: ⭐️⭐️⭐️⭐️🌟 MUST</p>



<p>This ride has a super long line of waiting at standby, about 2 hours wait. So I purchased the DPA and got in within 10 minutes. </p>



<p>Before you take on the ride, you will walk into the mansion of the beast and it looks just like in the movie!</p>



<p>This attraction is so well designed, your seat will take you within the rooms of the Beauty and the Beast mansion, take you through the story, and you will join the dance party with the beauty and the beast! It is very high tech and creative!</p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-5 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img decoding="async" data-id="654" src="https://static-assets.mszpro.com/2025/02/IMG_4885.heic" alt="" class="wp-image-654"/></figure>



<figure class="wp-block-image size-large"><img decoding="async" data-id="657" src="https://static-assets.mszpro.com/2025/02/IMG_4890.heic" alt="" class="wp-image-657"/></figure>



<figure class="wp-block-image size-large"><img decoding="async" data-id="656" src="https://static-assets.mszpro.com/2025/02/IMG_4893.heic" alt="" class="wp-image-656"/></figure>



<figure class="wp-block-image size-large"><img decoding="async" data-id="655" src="https://static-assets.mszpro.com/2025/02/IMG_4894.heic" alt="" class="wp-image-655"/></figure>



<figure class="wp-block-image size-large"><img decoding="async" data-id="658" src="https://static-assets.mszpro.com/2025/02/IMG_4899.heic" alt="" class="wp-image-658"/></figure>
</figure>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="【公式】美女と野獣“魔法のものがたり” | 東京ディズニーランド/Tokyo Disneyland" width="500" height="281" src="https://www.youtube.com/embed/plcgsCkU0xw?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<ul class="wp-block-list">
<li>The Happy Ride with Baymax</li>
</ul>



<p>Recommendation level: ⭐️⭐️⭐️⭐️🌟 MUST</p>



<p>This ride is so delight! The music is a hit! And it looks relaxing, but it gets exciting sometimes, with the car moving around quickly!</p>



<p>The wait is super long, around an hour, but a DPA gets me in within 5 minutes.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="561" src="https://static-assets.mszpro.com/2025/02/IMG_76C90B0CB32C-1-1024x561.jpeg" alt="" class="wp-image-659" srcset="https://static-assets.mszpro.com/2025/02/IMG_76C90B0CB32C-1-300x164.jpeg 300w, https://static-assets.mszpro.com/2025/02/IMG_76C90B0CB32C-1-1024x561.jpeg 1024w, https://static-assets.mszpro.com/2025/02/IMG_76C90B0CB32C-1-768x421.jpeg 768w, https://static-assets.mszpro.com/2025/02/IMG_76C90B0CB32C-1.jpeg 1320w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<ul class="wp-block-list">
<li>Club Mouse Beat</li>
</ul>



<p>For this one, you can try the &#8220;Standby Pass&#8221; option, since everyone has an assigned seat and there is no wait as like other attractions. However, Standby pass is like a lottery. I did not get the Standby pass, so I used DPA. But you should always try Standby pass first if it is available.</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 decoding="async" src="https://static-assets.mszpro.com/2025/02/IMG_4927.heic" alt="" class="wp-image-660"/></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-video"><video height="2160" style="aspect-ratio: 3840 / 2160;" width="3840" controls src="https://static-assets.mszpro.com/2025/02/IMG_4938.mov" class="mcloud-attachment-661"></video></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="576" src="https://static-assets.mszpro.com/2025/02/IMG_4932-1024x576.jpg" alt="" class="wp-image-664" srcset="https://static-assets.mszpro.com/2025/02/IMG_4932-300x169.jpg 300w, https://static-assets.mszpro.com/2025/02/IMG_4932-1024x576.jpg 1024w, https://static-assets.mszpro.com/2025/02/IMG_4932-768x432.jpg 768w, https://static-assets.mszpro.com/2025/02/IMG_4932-1536x864.jpg 1536w, https://static-assets.mszpro.com/2025/02/IMG_4932-2048x1152.jpg 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>
</div>



<ul class="wp-block-list">
<li>Parades</li>
</ul>



<p>There are many parades within the park. You can check it within the Disney Resort app by tapping on the list icon at the bottom right corner (besides the filter funnel) and see a list of shows.</p>



<figure class="wp-block-image size-full"><img decoding="async" src="https://static-assets.mszpro.com/2025/02/IMG_4956.heic" alt="" class="wp-image-663"/></figure>



<p>(I love the purple cat!)</p>



<ul class="wp-block-list">
<li>Mickey&#8217;s Magical Music World</li>
</ul>



<p>Recommendation level: ⭐️⭐️⭐️⭐️🌟 MUST</p>



<p>This is very very touching! You see many of your favorite Disney characters show up, including Judy and Nick from Zootopia, of course Mickey Donald, but also Frozen, the Caribbean&#8217;s, and many many more! It takes your memory back in time. And the music and the show are very beautiful.</p>



<figure class="wp-block-image size-full"><img decoding="async" src="https://static-assets.mszpro.com/2025/02/IMG_4958.heic" alt="" class="wp-image-662"/></figure>



<p>For this show, you need to reserve a seat via Standby pass or purchase a DPA, similar to other shows.</p>



<ul class="wp-block-list">
<li>Little World</li>
</ul>



<p>This is a very classic attraction! But there are now Marvel elements like Groot!</p>



<figure class="wp-block-video"><video height="2160" style="aspect-ratio: 3840 / 2160;" width="3840" controls src="https://static-assets.mszpro.com/2025/02/IMG_4945.mov" class="mcloud-attachment-666"></video></figure>



<figure class="wp-block-video"><video height="2160" style="aspect-ratio: 3840 / 2160;" width="3840" controls src="https://static-assets.mszpro.com/2025/02/IMG_4950.mov" class="mcloud-attachment-665"></video></figure>



<ul class="wp-block-list">
<li>Reach for the Stars fireworks show</li>
</ul>



<p>Recommendation level: ⭐️⭐️⭐️⭐️🌟 MUST</p>



<p>If you need a mental recharge, this is it. I basically cried, looking at all the childhood Disney characters, and listening to the Reach for the Stars (you&#8217;ll find further than you know, if you can dream it, you can do it). It is so inspiring!</p>



<figure class="wp-block-video"><video height="2160" style="aspect-ratio: 3840 / 2160;" width="3840" controls src="https://static-assets.mszpro.com/2025/02/IMG_4965.mov" class="mcloud-attachment-668"></video></figure>



<figure class="wp-block-video"><video height="2160" style="aspect-ratio: 3840 / 2160;" width="3840" controls src="https://static-assets.mszpro.com/2025/02/IMG_4968.mov" class="mcloud-attachment-669"></video></figure>



<figure class="wp-block-video"><video height="2160" style="aspect-ratio: 3840 / 2160;" width="3840" controls src="https://static-assets.mszpro.com/2025/02/IMG_4970.mov" class="mcloud-attachment-670"></video></figure>



<figure class="wp-block-video"><video height="2160" style="aspect-ratio: 3840 / 2160;" width="3840" controls src="https://static-assets.mszpro.com/2025/02/IMG_4971.mov" class="mcloud-attachment-671"></video></figure>



<figure class="wp-block-video"><video height="2160" style="aspect-ratio: 3840 / 2160;" width="3840" controls src="https://static-assets.mszpro.com/2025/02/IMG_4972.mov" class="mcloud-attachment-672"></video></figure>



<figure class="wp-block-video"><video height="2160" style="aspect-ratio: 3840 / 2160;" width="3840" controls src="https://static-assets.mszpro.com/2025/02/IMG_4973.mov" class="mcloud-attachment-673"></video></figure><p>The post <a href="https://mszpro.com/tokyo-disney-2025">Tokyo Disney 2-day travel guide – DisneySea & Land 2025 February – 15 Attractions</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></content:encoded>
					
		
		<enclosure url="https://static-assets.mszpro.com/2025/02/IMG_4938.mov" length="7071924" type="video/quicktime" />
<enclosure url="https://static-assets.mszpro.com/2025/02/IMG_4945.mov" length="16475635" type="video/quicktime" />
<enclosure url="https://static-assets.mszpro.com/2025/02/IMG_4950.mov" length="8552962" type="video/quicktime" />
<enclosure url="https://static-assets.mszpro.com/2025/02/IMG_4965.mov" length="35487962" type="video/quicktime" />
<enclosure url="https://static-assets.mszpro.com/2025/02/IMG_4968.mov" length="24562991" type="video/quicktime" />
<enclosure url="https://static-assets.mszpro.com/2025/02/IMG_4970.mov" length="94695222" type="video/quicktime" />
<enclosure url="https://static-assets.mszpro.com/2025/02/IMG_4971.mov" length="25295193" type="video/quicktime" />
<enclosure url="https://static-assets.mszpro.com/2025/02/IMG_4972.mov" length="18794511" type="video/quicktime" />
<enclosure url="https://static-assets.mszpro.com/2025/02/IMG_4973.mov" length="43603661" type="video/quicktime" />

			</item>
		<item>
		<title>88&#215;31 logos</title>
		<link>https://mszpro.com/88x31-logos</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Wed, 25 Dec 2024 13:50:26 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=573</guid>

					<description><![CDATA[<p>Large (6.6MB) Medium (1.8 MB) Small (772 kb)</p>
<p>The post <a href="https://mszpro.com/88x31-logos">88×31 logos</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Large (6.6MB)</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="359" src="https://static-assets.mszpro.com/2024/12/mszpro-88x31-high-quality-1024x359.gif" alt="" class="wp-image-574" srcset="https://static-assets.mszpro.com/2024/12/mszpro-88x31-high-quality-300x105.gif 300w, https://static-assets.mszpro.com/2024/12/mszpro-88x31-high-quality-1024x359.gif 1024w, https://static-assets.mszpro.com/2024/12/mszpro-88x31-high-quality-768x269.gif 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>Medium (1.8 MB)</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="622" height="218" src="https://static-assets.mszpro.com/2024/12/mszpro-88x31-medium-quality.gif" alt="" class="wp-image-575"/></figure>



<p>Small (772 kb)</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="416" height="146" src="https://static-assets.mszpro.com/2024/12/mszpro-88x31-low-quality.gif" alt="" class="wp-image-576"/></figure><p>The post <a href="https://mszpro.com/88x31-logos">88×31 logos</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Extract picked Memoji and stickers from UITextView (+SwiftUI compatible view)</title>
		<link>https://mszpro.com/memoji-textfield-picking</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Sat, 21 Dec 2024 13:08:06 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=569</guid>

					<description><![CDATA[<p>Sometimes, you might want to allow the user to pick their Memoji and stickers and upload them within your own app. To do that, you can present a keyboard and show Memojis and stickers. They will show on the keyboard if you have set the following properties for your UITextView: Then, you will set a [&#8230;]</p>
<p>The post <a href="https://mszpro.com/memoji-textfield-picking">Extract picked Memoji and stickers from UITextView (+SwiftUI compatible view)</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Sometimes, you might want to allow the user to pick their Memoji and stickers and upload them within your own app.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="936" height="1024" src="https://static-assets.mszpro.com/2024/12/IMG_1C4A90F53238-1-936x1024.jpeg" alt="" class="wp-image-570" srcset="https://static-assets.mszpro.com/2024/12/IMG_1C4A90F53238-1-274x300.jpeg 274w, https://static-assets.mszpro.com/2024/12/IMG_1C4A90F53238-1-936x1024.jpeg 936w, https://static-assets.mszpro.com/2024/12/IMG_1C4A90F53238-1-768x840.jpeg 768w, https://static-assets.mszpro.com/2024/12/IMG_1C4A90F53238-1.jpeg 1320w" sizes="auto, (max-width: 936px) 100vw, 936px" /></figure>



<p>To do that, you can present a keyboard and show Memojis and stickers. They will show on the keyboard if you have set the following properties for your UITextView:</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="textView.supportsAdaptiveImageGlyph = true
textView.allowsEditingTextAttributes = true" 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: #D8DEE9">textView</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">supportsAdaptiveImageGlyph</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: #D8DEE9">textView</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">allowsEditingTextAttributes</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true</span></span></code></pre></div>



<p>Then, you will set a UITextView delegate <em>UITextViewDelegate</em> to receive a call whenever the content changed.</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="textView.delegate = self" 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: #D8DEE9">textView</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">delegate</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">self</span></span></code></pre></div>



<p>Conform the view controller to <em>UITextViewDelegate</em>. Then implement the textViewDidChange function</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="func textViewDidChange(_ textView: UITextView) {
    if let attachment = findFirstAttachment(in: textView.attributedText) {
        handleMemoji(attachment: attachment)
        textView.text = &quot;&quot;
        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: #D8DEE9">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">textViewDidChange</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">textView</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">UITextView</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">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">attachment</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findFirstAttachment</span><span style="color: #D8DEE9FF">(</span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">textView</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">attributedText</span><span style="color: #D8DEE9FF">) </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">handleMemoji</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">attachment</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> attachment</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">        textView.text = &quot;&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        return</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>First, we try to find the attachment object that has Memoji within. We first check by type <em>adaptiveImageGlyph</em> (only available for iOS 18 and up), which is usually the case when you pick a sticker within iOS 18 system. And we check for <em>attachment</em> too.</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="private func findFirstAttachment(in attributedText: NSAttributedString?) -&gt; NSTextAttachment? {
    guard let attributedText else { return nil }
    
    // First try to find NSAdaptiveImageGlyph
    var foundGlyph: NSTextAttachment?
    attributedText.enumerateAttribute(.adaptiveImageGlyph,
                                      in: NSRange(location: 0, length: attributedText.length),
                                      options: []) { value, range, stop in
        if let glyph = value as? NSAdaptiveImageGlyph {
            let attachment = NSTextAttachment()
            attachment.image = UIImage(data: glyph.imageContent)
            foundGlyph = attachment
            stop.pointee = true
        }
    }
    
    if let foundGlyph { return foundGlyph }
    
    // Fallback to regular attachment
    var foundAttachment: NSTextAttachment?
    attributedText.enumerateAttribute(.attachment,
                                      in: NSRange(location: 0, length: attributedText.length),
                                      options: []) { value, range, stop in
        if let attachment = value as? NSTextAttachment {
            foundAttachment = attachment
            stop.pointee = true
        }
    }
    return foundAttachment
}" 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: #D8DEE9">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findFirstAttachment</span><span style="color: #D8DEE9FF">(</span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">attributedText</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">NSAttributedString</span><span style="color: #81A1C1">?</span><span style="color: #D8DEE9FF">) </span><span style="color: #81A1C1">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">NSTextAttachment</span><span style="color: #81A1C1">?</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    guard let attributedText else </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: #D8DEE9">nil</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">// First try to find NSAdaptiveImageGlyph</span></span>
<span class="line"><span style="color: #D8DEE9FF">    var </span><span style="color: #88C0D0">foundGlyph</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">NSTextAttachment</span><span style="color: #81A1C1">?</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D8DEE9">attributedText</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">enumerateAttribute</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">adaptiveImageGlyph</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                      </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF">: </span><span style="color: #88C0D0">NSRange</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">location</span><span style="color: #D8DEE9FF">: </span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">length</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">attributedText</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">length)</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                      </span><span style="color: #D8DEE9">options</span><span style="color: #D8DEE9FF">: []) </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">value</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">range</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> stop </span><span style="color: #D8DEE9">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">        if let </span><span style="color: #D8DEE9">glyph</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">value</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">as</span><span style="color: #81A1C1">?</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">NSAdaptiveImageGlyph</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            let </span><span style="color: #D8DEE9">attachment</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">NSTextAttachment</span><span style="color: #D8DEE9FF">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">            attachment.</span><span style="color: #D8DEE9">image</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: #D8DEE9FF">(</span><span style="color: #D8DEE9">data</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">glyph</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">imageContent</span><span style="color: #D8DEE9FF">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #D8DEE9">foundGlyph</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">attachment</span></span>
<span class="line"><span style="color: #D8DEE9FF">            stop.</span><span style="color: #D8DEE9">pointee</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: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    }</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D8DEE9">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">foundGlyph</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> return </span><span style="color: #D8DEE9">foundGlyph</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">// Fallback to regular attachment</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D8DEE9">var</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">foundAttachment</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">NSTextAttachment</span><span style="color: #81A1C1">?</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D8DEE9">attributedText</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">enumerateAttribute</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">attachment</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                      </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF">: </span><span style="color: #88C0D0">NSRange</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">location</span><span style="color: #D8DEE9FF">: </span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">length</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">attributedText</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">length)</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                      </span><span style="color: #D8DEE9">options</span><span style="color: #D8DEE9FF">: []) </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">value</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">range</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> stop </span><span style="color: #D8DEE9">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">        if let </span><span style="color: #D8DEE9">attachment</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">value</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">as</span><span style="color: #81A1C1">?</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">NSTextAttachment</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #D8DEE9">foundAttachment</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">attachment</span></span>
<span class="line"><span style="color: #D8DEE9FF">            stop.</span><span style="color: #D8DEE9">pointee</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: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    }</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D8DEE9">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">foundAttachment</span></span>
<span class="line"><span style="color: #D8DEE9FF">}</span></span></code></pre></div>



<p>Then, if we have found such an text attachment, we extract the image:</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="private func handleMemoji(attachment: NSTextAttachment) {
    if let image = attachment.image {
        self.pickedImage = image
    } else if let image = attachment.image(forBounds: attachment.bounds,
                                           textContainer: nil,
                                           characterIndex: 0) {
        self.pickedImage = image
    } else if let imageData = attachment.fileWrapper?.regularFileContents,
              let image = UIImage(data: imageData) {
        self.pickedImage = image
    }
    
#if DEBUG
    print(&quot;Memoji attachment handled: \(String(describing: self.pickedImage))&quot;)
#endif
}" 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: #D8DEE9">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">handleMemoji</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">attachment</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">NSTextAttachment</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">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">image</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">attachment</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">image</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        self.</span><span style="color: #D8DEE9">pickedImage</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">image</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">image</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">attachment</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">image</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">forBounds</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">attachment</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">bounds</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                           </span><span style="color: #D8DEE9">textContainer</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">nil</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                           </span><span style="color: #D8DEE9">characterIndex</span><span style="color: #D8DEE9FF">: </span><span style="color: #B48EAD">0</span><span style="color: #D8DEE9FF">) </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        self.</span><span style="color: #D8DEE9">pickedImage</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">image</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">imageData</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">attachment</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">fileWrapper</span><span style="color: #ECEFF4">?.</span><span style="color: #D8DEE9">regularFileContents</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">              </span><span style="color: #D8DEE9">let</span><span style="color: #D8DEE9FF"> image </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">UIImage</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">data</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">imageData</span><span style="color: #D8DEE9FF">) </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        self.</span><span style="color: #D8DEE9">pickedImage</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">image</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">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">DEBUG</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">print</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Memoji attachment handled: </span><span style="color: #EBCB8B">\(</span><span style="color: #A3BE8C">String(describing: self.pickedImage))</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">#</span><span style="color: #D8DEE9">endif</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>The below shows an example of a SwiftUI compatible view. If you are using UIKit, simple implement the delegate for your UITextView.</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: - StickerPickingTextView
@available(iOS 18.0, *)
struct StickerPickingTextView: UIViewRepresentable {
    @Binding var pickedImage: UIImage?
    @Binding var pickedEmoji: String
    
    func makeUIView(context: Context) -&gt; AdaptiveEmojiTextView {
        let textView = UITextView()
        textView.supportsAdaptiveImageGlyph = true
        textView.allowsEditingTextAttributes = true
        textView.delegate = context.coordinator
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) { return }
    
    func makeCoordinator() -&gt; Coordinator {
        Coordinator(pickedImage: $pickedImage, pickedEmoji: $pickedEmoji)
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        @Binding var pickedImage: UIImage?
        @Binding var pickedEmoji: String
        
        init(pickedImage: Binding&lt;UIImage?&gt;, pickedEmoji: Binding&lt;String&gt;) {
            self._pickedImage = pickedImage
            self._pickedEmoji = pickedEmoji
        }
        
        func textView(_ textView: UITextView,
                      shouldChangeTextIn range: NSRange,
                      replacementText text: String) -&gt; Bool {
            let newLength = (textView.text?.count ?? 0) + text.count - range.length
            return newLength &lt;= 1
        }
        
        func textViewDidChange(_ textView: UITextView) {
            // Handle Memoji and adaptive image glyphs
            if let attachment = findFirstAttachment(in: textView.attributedText) {
                handleMemoji(attachment: attachment)
                textView.text = &quot;&quot;
                return
            }
            
            // Handle regular emoji
            if let text = textView.text, !text.isEmpty {
                handleEmoji(text)
                textView.text = &quot;&quot;
            }
        }
        
        private func findFirstAttachment(in attributedText: NSAttributedString?) -&gt; NSTextAttachment? {
            guard let attributedText else { return nil }
            
            // First try to find NSAdaptiveImageGlyph
            var foundGlyph: NSTextAttachment?
            attributedText.enumerateAttribute(.adaptiveImageGlyph,
                                              in: NSRange(location: 0, length: attributedText.length),
                                              options: []) { value, range, stop in
                if let glyph = value as? NSAdaptiveImageGlyph {
                    let attachment = NSTextAttachment()
                    attachment.image = UIImage(data: glyph.imageContent)
                    foundGlyph = attachment
                    stop.pointee = true
                }
            }
            
            if let foundGlyph { return foundGlyph }
            
            // Fallback to regular attachment
            var foundAttachment: NSTextAttachment?
            attributedText.enumerateAttribute(.attachment,
                                              in: NSRange(location: 0, length: attributedText.length),
                                              options: []) { value, range, stop in
                if let attachment = value as? NSTextAttachment {
                    foundAttachment = attachment
                    stop.pointee = true
                }
            }
            return foundAttachment
        }
        
        private func handleMemoji(attachment: NSTextAttachment) {
            if let image = attachment.image {
                self.pickedImage = image
            } else if let image = attachment.image(forBounds: attachment.bounds,
                                                   textContainer: nil,
                                                   characterIndex: 0) {
                self.pickedImage = image
            } else if let imageData = attachment.fileWrapper?.regularFileContents,
                      let image = UIImage(data: imageData) {
                self.pickedImage = image
            }
            
#if DEBUG
            print(&quot;Memoji attachment handled: \(String(describing: self.pickedImage))&quot;)
#endif
        }
        
        private func handleEmoji(_ text: String) {
            self.pickedEmoji = text
            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
                                            to: nil,
                                            from: nil,
                                            for: nil)
        }
    }
}
" 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: - StickerPickingTextView</span></span>
<span class="line"><span style="color: #D08770">@available</span><span style="color: #D8DEE9FF">(</span><span style="color: #D08770">iOS</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">18.0</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">*</span><span style="color: #D8DEE9FF">)</span></span>
<span class="line"><span style="color: #D8DEE9">struct</span><span style="color: #D8DEE9FF"> StickerPickingTextView</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">UIViewRepresentable</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D08770">@Binding</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pickedImage</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> UIImage</span><span style="color: #81A1C1">?</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D08770">@Binding</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">var</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pickedEmoji</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> String</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D8DEE9">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">makeUIView</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">context</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">Context</span><span style="color: #D8DEE9FF">) </span><span style="color: #81A1C1">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">AdaptiveEmojiTextView</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"> </span><span style="color: #D8DEE9">textView</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">UITextView</span><span style="color: #D8DEE9FF">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #D8DEE9">textView</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">supportsAdaptiveImageGlyph</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: #D8DEE9">textView</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">allowsEditingTextAttributes</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: #D8DEE9">textView</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">delegate</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">coordinator</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">textView</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: #D8DEE9">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">updateUIView</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">uiView</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">UITextView</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">context</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">Context</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: #D8DEE9">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">makeCoordinator</span><span style="color: #D8DEE9FF">() </span><span style="color: #81A1C1">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Coordinator</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">Coordinator</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">pickedImage</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">$pickedImage</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pickedEmoji</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">$pickedEmoji</span><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>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Coordinator</span><span style="color: #D8DEE9FF">: </span><span style="color: #8FBCBB">NSObject</span><span style="color: #D8DEE9FF">, </span><span style="color: #8FBCBB">UITextViewDelegate</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #D08770">@Binding</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">var</span><span style="color: #D8DEE9FF"> pickedImage</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> UIImage</span><span style="color: #81A1C1">?</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #D08770">@Binding</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">var</span><span style="color: #D8DEE9FF"> pickedEmoji</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> String</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">init</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">pickedImage</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> Binding</span><span style="color: #ECEFF4">&lt;</span><span style="color: #D8DEE9FF">UIImage</span><span style="color: #81A1C1">?</span><span style="color: #ECEFF4">&gt;,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pickedEmoji</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> Binding</span><span style="color: #ECEFF4">&lt;</span><span style="color: #D8DEE9FF">String</span><span style="color: #ECEFF4">&gt;)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #D8DEE9">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">_pickedImage</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pickedImage</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #D8DEE9">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">_pickedEmoji</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pickedEmoji</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: #D8DEE9">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">textView</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">textView</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> UITextView</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                      </span><span style="color: #D8DEE9">shouldChangeTextIn</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">range</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> NSRange</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                      </span><span style="color: #D8DEE9">replacementText</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">text</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> String</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> -&gt; Bool </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"> </span><span style="color: #D8DEE9">newLength</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> (</span><span style="color: #D8DEE9">textView</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">text</span><span style="color: #ECEFF4">?.</span><span style="color: #D8DEE9">count</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">??</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">0</span><span style="color: #D8DEE9FF">) </span><span style="color: #81A1C1">+</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">text</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">count</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">range</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">length</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">newLength</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&lt;=</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1</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: #D8DEE9">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">textViewDidChange</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">textView</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> UITextView</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">// Handle Memoji and adaptive image glyphs</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"> </span><span style="color: #D8DEE9">attachment</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findFirstAttachment</span><span style="color: #D8DEE9FF">(</span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">textView</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">attributedText</span><span style="color: #D8DEE9FF">) </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">handleMemoji</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">attachment</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> attachment</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                textView.text = &quot;&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">                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 style="color: #616E88">// Handle regular emoji</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"> </span><span style="color: #D8DEE9">text</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">textView</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">text</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> !</span><span style="color: #D8DEE9">text</span><span style="color: #D8DEE9FF">.isEmpty {</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">handleEmoji</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">text</span><span style="color: #D8DEE9FF">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #D8DEE9">textView</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">text</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;&quot;</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: #D8DEE9">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findFirstAttachment</span><span style="color: #D8DEE9FF">(</span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">attributedText</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">NSAttributedString</span><span style="color: #81A1C1">?</span><span style="color: #D8DEE9FF">) </span><span style="color: #81A1C1">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">NSTextAttachment</span><span style="color: #81A1C1">?</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            guard let attributedText else </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: #D8DEE9">nil</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">// First try to find NSAdaptiveImageGlyph</span></span>
<span class="line"><span style="color: #D8DEE9FF">            var </span><span style="color: #88C0D0">foundGlyph</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">NSTextAttachment</span><span style="color: #81A1C1">?</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #D8DEE9">attributedText</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">enumerateAttribute</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">adaptiveImageGlyph</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                              </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF">: </span><span style="color: #88C0D0">NSRange</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">location</span><span style="color: #D8DEE9FF">: </span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">length</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">attributedText</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">length)</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                              </span><span style="color: #D8DEE9">options</span><span style="color: #D8DEE9FF">: []) </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">value</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">range</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> stop </span><span style="color: #D8DEE9">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">                if let </span><span style="color: #D8DEE9">glyph</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">value</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">as</span><span style="color: #81A1C1">?</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">NSAdaptiveImageGlyph</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    let </span><span style="color: #D8DEE9">attachment</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">NSTextAttachment</span><span style="color: #D8DEE9FF">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    attachment.</span><span style="color: #D8DEE9">image</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: #D8DEE9FF">(</span><span style="color: #D8DEE9">data</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">glyph</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">imageContent</span><span style="color: #D8DEE9FF">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #D8DEE9">foundGlyph</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">attachment</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    stop.</span><span style="color: #D8DEE9">pointee</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: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            }</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #D8DEE9">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">foundGlyph</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> return </span><span style="color: #D8DEE9">foundGlyph</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">// Fallback to regular attachment</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #D8DEE9">var</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">foundAttachment</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">NSTextAttachment</span><span style="color: #81A1C1">?</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #D8DEE9">attributedText</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">enumerateAttribute</span><span style="color: #D8DEE9FF">(</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">attachment</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                              </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF">: </span><span style="color: #88C0D0">NSRange</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">location</span><span style="color: #D8DEE9FF">: </span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">length</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">attributedText</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">length)</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                              </span><span style="color: #D8DEE9">options</span><span style="color: #D8DEE9FF">: []) </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">value</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">range</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> stop </span><span style="color: #D8DEE9">in</span></span>
<span class="line"><span style="color: #D8DEE9FF">                if let </span><span style="color: #D8DEE9">attachment</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">value</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">as</span><span style="color: #81A1C1">?</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">NSTextAttachment</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #D8DEE9">foundAttachment</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">attachment</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    stop.</span><span style="color: #D8DEE9">pointee</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: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            }</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #D8DEE9">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">foundAttachment</span></span>
<span class="line"><span style="color: #D8DEE9FF">        }</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #D8DEE9">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">handleMemoji</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">attachment</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">NSTextAttachment</span><span style="color: #D8DEE9FF">) </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            if let </span><span style="color: #D8DEE9">image</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">attachment</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">image</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                self.</span><span style="color: #D8DEE9">pickedImage</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">image</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">image</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">attachment</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">image</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">forBounds</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">attachment</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">bounds</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                   </span><span style="color: #D8DEE9">textContainer</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">nil</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                                   </span><span style="color: #D8DEE9">characterIndex</span><span style="color: #D8DEE9FF">: </span><span style="color: #B48EAD">0</span><span style="color: #D8DEE9FF">) </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                self.</span><span style="color: #D8DEE9">pickedImage</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">image</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">let</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">imageData</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">attachment</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">fileWrapper</span><span style="color: #ECEFF4">?.</span><span style="color: #D8DEE9">regularFileContents</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                      let </span><span style="color: #D8DEE9">image</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: #D8DEE9FF">(</span><span style="color: #D8DEE9">data</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">imageData</span><span style="color: #D8DEE9FF">) </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                self.</span><span style="color: #D8DEE9">pickedImage</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">image</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">#if </span><span style="color: #D8DEE9">DEBUG</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">Memoji attachment handled: </span><span style="color: #EBCB8B">\(</span><span style="color: #A3BE8C">String(describing: self.pickedImage))</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">#endif</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: #D8DEE9">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">func</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">handleEmoji</span><span style="color: #D8DEE9FF">(</span><span style="color: #D8DEE9">_</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">text</span><span style="color: #D8DEE9FF">: </span><span style="color: #D8DEE9">String</span><span style="color: #D8DEE9FF">) </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            self.</span><span style="color: #D8DEE9">pickedEmoji</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">text</span></span>
<span class="line"><span style="color: #D8DEE9FF">            UIApplication.shared.sendAction(#</span><span style="color: #88C0D0">selector</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">UIResponder.resignFirstResponder</span><span style="color: #ECEFF4">),</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                            </span><span style="color: #88C0D0">to</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">nil</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                                            </span><span style="color: #88C0D0">from</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">nil</span><span style="color: #ECEFF4">,</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">nil</span><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>
<span class="line"><span style="color: #D8DEE9FF">}</span></span>
<span class="line"></span></code></pre></div><p>The post <a href="https://mszpro.com/memoji-textfield-picking">Extract picked Memoji and stickers from UITextView (+SwiftUI compatible view)</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<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">AsterKit Software Limited</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 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-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="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<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-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="auto, (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 loading="lazy" 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="auto, (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">AsterKit Software Limited</a>.</p>]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Mixi2 招待</title>
		<link>https://mszpro.com/mixi2-%e6%8b%9b%e5%be%85</link>
		
		<dc:creator><![CDATA[msz]]></dc:creator>
		<pubDate>Tue, 17 Dec 2024 04:24:49 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://mszpro.com/?p=513</guid>

					<description><![CDATA[<p>&#8220;MIXIが提供する「繋がった人、繋がりたい人との関係性を深められる」ことを目指した新しいSNSです。&#8221; 招待コード：一緒にはじめよう！🚀https://mixi.social/invitations/@mszpro/YbHJh9E4wF8kKaY8hARm6w mszproからの #mixi2 招待🎟️一緒にはじめよう！🚀https://mixi.social/invitations/@mszpro/YbHJh9E4wF8kKaY8hARm6w</p>
<p>The post <a href="https://mszpro.com/mixi2-%e6%8b%9b%e5%be%85">Mixi2 招待</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>&#8220;MIXIが提供する「繋がった人、繋がりたい人との関係性を深められる」ことを目指した新しいSNSです。&#8221;</p>



<p>招待コード：<br>一緒にはじめよう！🚀<br><a href="https://mixi.social/invitations/@mszpro/YbHJh9E4wF8kKaY8hARm6w">https://mixi.social/invitations/@mszpro/YbHJh9E4wF8kKaY8hARm6w</a></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="504" height="1024" src="https://static-assets.mszpro.com/2024/12/IMG_08D15755F423-1-504x1024.jpeg" alt="" class="wp-image-514" srcset="https://static-assets.mszpro.com/2024/12/IMG_08D15755F423-1-148x300.jpeg 148w, https://static-assets.mszpro.com/2024/12/IMG_08D15755F423-1-504x1024.jpeg 504w, https://static-assets.mszpro.com/2024/12/IMG_08D15755F423-1-768x1560.jpeg 768w, https://static-assets.mszpro.com/2024/12/IMG_08D15755F423-1-756x1536.jpeg 756w, https://static-assets.mszpro.com/2024/12/IMG_08D15755F423-1-1008x2048.jpeg 1008w, https://static-assets.mszpro.com/2024/12/IMG_08D15755F423-1-scaled.jpeg 1260w" sizes="auto, (max-width: 504px) 100vw, 504px" /></figure>



<p>mszproからの #mixi2 招待🎟️<br>一緒にはじめよう！🚀<br><a href="https://mixi.social/invitations/@mszpro/YbHJh9E4wF8kKaY8hARm6w">https://mixi.social/invitations/@mszpro/YbHJh9E4wF8kKaY8hARm6w</a></p>



<p></p><p>The post <a href="https://mszpro.com/mixi2-%e6%8b%9b%e5%be%85">Mixi2 招待</a> first appeared on <a href="https://mszpro.com">AsterKit Software Limited</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 16/104 queries in 0.031 seconds using Disk

Served from: mszpro.com @ 2025-12-31 03:12:45 by W3 Total Cache
-->