Kyle Howells Blog
https://ikyle.me/blog/recent.xml
2023-02-09T03:54:05.107912+00:00
Kyle Howells
https://ikyle.me
kyle@ikyle.me
© 2023 Kyle Howells
Werkzeug
How to Turn On an LG C2 with Home Assistant
https://ikyle.me/blog/2023/how-to-turn-on-lg-c2-with-home-assistant
2023-02-09T03:54:05.107912+00:00
2023-02-09T03:54:05.107912+00:00
Setting up WakeOnLAN and configuring input sources for an LG C2 in Home Assistant
<p><a href="https://www.home-assistant.io">HomeAssistant</a> automatically detects and supports LG webOS smart TVs with the <a href="https://www.home-assistant.io/integrations/webostv">LG webOS Smart TV integration</a>.</p>
<p><img src="https://ikyle.me/blog/2023/how-to-turn-on-lg-c2-with-home-assistant/1-auto-detect.jpg" alt="" /></p>
<h1>Turn On TV</h1>
<p>However, by default you can turn the TV off, but not back on again.<br />
In order to turn the TV on you need to manually setup an action on the device which calls the <a href="https://www.home-assistant.io/integrations/webostv#turn-on-action">WakeOnLan service</a>.</p>
<p>As far as I understand, the reason for this is because the <code>WakeOnLan</code> service is a separate integration and integrations can't install/call each other automatically.</p>
<h1>Setup</h1>
<p>1: Enable the <a href="https://www.home-assistant.io/integrations/wake_on_lan/">WakeOnLan</a> integration.<br />
(requires editing the <code>configuration.yaml</code> manually).</p>
<p>2: Inside the device details for the TV.<br />
Create a new automation triggered by <code>Device is requested to turn on</code>.</p>
<p><img src="https://ikyle.me/blog/2023/how-to-turn-on-lg-c2-with-home-assistant/2-automation.jpg" alt="" /></p>
<p>3: Add a <code>Call service</code> action.</p>
<p><img src="https://ikyle.me/blog/2023/how-to-turn-on-lg-c2-with-home-assistant/3-call-service.jpg" alt="" /></p>
<p>4: Select call <code>Wake on LAN</code> send magic packet</p>
<p><img src="https://ikyle.me/blog/2023/how-to-turn-on-lg-c2-with-home-assistant/4-wake-on-lan.jpg" alt="" /></p>
<p>5: Add the MAC address of the TV.</p>
<p>The instructions on the Home Assistant documentation doesn't mention this, but for me to get this to work I also needed to fill in the <code>Broadcast address</code> and set it to the local subnet's broadcast IP, and unselect the <code>Broadcast port</code>.</p>
<p>As my IP address range is <code>192.168.50.1-254</code> this is <code>192.168.50.255</code> for me.<br />
For you it will likely be either <code>192.168.0.255</code> or <code>192.168.1.255</code>.</p>
<p><img src="https://ikyle.me/blog/2023/how-to-turn-on-lg-c2-with-home-assistant/5-wake-on-lan-setup.jpg" alt="" /></p>
<p>This automation should be automatically picked up by the webOS integration and means you will now have fully power ON and OFF controls for the TV device.</p>
<h1>Finished Setup</h1>
<p><img src="https://ikyle.me/blog/2023/how-to-turn-on-lg-c2-with-home-assistant/6-lg-c2-home-assistant-control.jpg" alt="" /></p>
Creating a CoreImage Filter with a Metal Shader
https://ikyle.me/blog/2022/creating-a-coreimage-filter-with-a-metal-kernel
2022-07-03T00:56:34.030222+00:00
2022-07-03T00:56:34.030222+00:00
How to setup Xcode to build a Metal Shader for use with a custom CoreImage Kernel
<p>In my recent article explaining how to make a <a href="https://ikyle.me/blog/2022/creating-cool-ui-shape-morphing">metaball style iOS Shape Morphing effect</a> I glossed over how to use a Metal CoreImage filter; instead of using the deprecated Core Image Kernel Language Shader.</p>
<p>The reason for glossing over it because it is annoyingly complicated!</p>
<p>Where as showing you how to use the Core Image Kernel Language Shader based shader as as simple as this:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// LumaThresholdFilter.swift</span>
<span style="color: #A90D91">class</span> <span style="color: #3F6E75">LumaThresholdFilter</span>: <span style="color: #5B269A">CIFilter</span>
{
<span style="color: #A90D91">var</span> <span style="color: #000000">threshold</span>: <span style="color: #000000">CGFloat</span> = <span style="color: #1C01CE">0.5</span>
<span style="color: #A90D91">static</span> <span style="color: #A90D91">let</span> <span style="color: #000000">thresholdKernel</span> = <span style="color: #5B269A">CIColorKernel</span>(<span style="color: #000000">source</span>:<span style="color: #C41A16">"""</span>
<span style="color: #C41A16">kernel vec4 thresholdFilter(__sample image, float threshold)</span>
<span style="color: #C41A16">{</span>
<span style="color: #C41A16"> float luma = (image.r * 0.2126) + (image.g * 0.7152) + (image.b * 0.0722);</span>
<span style="color: #C41A16"> return (luma > threshold) ? vec4(1.0, 1.0, 1.0, 1.0) : vec4(0.0, 0.0, 0.0, 0.0);</span>
<span style="color: #C41A16">}</span>
<span style="color: #C41A16">"""</span>)<span style="color: #000000">!</span>
<span style="color: #177500">//</span>
<span style="color: #A90D91">@objc</span> <span style="color: #A90D91">dynamic</span> <span style="color: #A90D91">var</span> <span style="color: #000000">inputImage</span> : <span style="color: #5B269A">CIImage</span>?
<span style="color: #A90D91">override</span> <span style="color: #A90D91">var</span> <span style="color: #000000">outputImage</span> : <span style="color: #5B269A">CIImage</span>!
{
<span style="color: #A90D91">guard</span> <span style="color: #A90D91">let</span> <span style="color: #000000">inputImage</span> = <span style="color: #A90D91">self</span>.<span style="color: #000000">inputImage</span> <span style="color: #A90D91">else</span>
{
<span style="color: #A90D91">return</span> <span style="color: #A90D91">nil</span>
}
<span style="color: #A90D91">let</span> <span style="color: #000000">arguments</span> = [<span style="color: #000000">inputImage</span>, <span style="color: #A90D91">Float</span>(<span style="color: #A90D91">self</span>.<span style="color: #000000">threshold</span>)] <span style="color: #A90D91">as</span> [<span style="color: #A90D91">Any</span>]
<span style="color: #A90D91">return</span> <span style="color: #A90D91">Self</span>.<span style="color: #000000">thresholdKernel</span>.<span style="color: #000000">apply</span>(<span style="color: #000000">extent</span>: <span style="color: #000000">inputImage</span>.<span style="color: #000000">extent</span>, <span style="color: #000000">arguments</span>: <span style="color: #000000">arguments</span>)
}
}
</code></pre></div>
<p>To explain how to use the Metal shader version requires quite a few more steps.</p>
<h1>Custom Core Image Filters with Metal Shaders</h1>
<p>Unlike with Core Image Kernel Language shaders you can't just include the shader as a string. Instead them most be in separate <code>.metal</code> files.</p>
<p>This has positives: like compile time error checking; and negatives: no tricks involving dynamic manipulation/generation of shaders.</p>
<p>So to start with we'll move our <code>thresholdFilter</code> shader function from the string into its own file <code>LumaThreshold.metal</code>.<br />
Except no. We'll actually name it <code>LumaThreshold.ci.metal</code> not because we have to, but because it'll help us later.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// LumaThreshold.ci.metal</span>
<span style="color: #633820">#include</span> <span style="color: #177500"><metal_stdlib></span><span style="color: #633820"></span>
<span style="color: #A90D91">using</span> <span style="color: #A90D91">namespace</span> <span style="color: #000000">metal</span>;
<span style="color: #633820">#include</span> <span style="color: #177500"><CoreImage/CoreImage.h></span><span style="color: #633820"></span>
<span style="color: #A90D91">extern</span> <span style="color: #C41A16">"C"</span> <span style="color: #000000">float4</span> <span style="color: #000000">lumaThreshold</span>(<span style="color: #000000">coreimage::sample_t</span> <span style="color: #000000">pixelColor</span>, <span style="color: #A90D91">float</span> <span style="color: #000000">threshold</span>, <span style="color: #000000">coreimage::destination</span> <span style="color: #000000">destination</span>)
{
<span style="color: #000000">float3</span> <span style="color: #000000">pixelRGB</span> <span style="color: #000000">=</span> <span style="color: #000000">pixelColor</span>.<span style="color: #000000">rgb</span>;
<span style="color: #A90D91">float</span> <span style="color: #000000">luma</span> <span style="color: #000000">=</span> (<span style="color: #000000">pixelRGB</span>.<span style="color: #000000">r</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">0.2126</span>) <span style="color: #000000">+</span> (<span style="color: #000000">pixelRGB</span>.<span style="color: #000000">g</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">0.7152</span>) <span style="color: #000000">+</span> (<span style="color: #000000">pixelRGB</span>.<span style="color: #000000">b</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">0.0722</span>);
<span style="color: #A90D91">return</span> (<span style="color: #000000">luma</span> <span style="color: #000000">></span> <span style="color: #000000">threshold</span>) <span style="color: #000000">?</span> <span style="color: #000000">float4</span>(<span style="color: #1C01CE">1.0</span>, <span style="color: #1C01CE">1.0</span>, <span style="color: #1C01CE">1.0</span>, <span style="color: #1C01CE">1.0</span>) <span style="color: #000000">:</span> <span style="color: #000000">float4</span>(<span style="color: #1C01CE">0.0</span>, <span style="color: #1C01CE">0.0</span>, <span style="color: #1C01CE">0.0</span>, <span style="color: #1C01CE">0.0</span>);
}
</code></pre></div>
<p>Now to load this in our CoreImage filter subclass we'll load it like this:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">static</span> <span style="color: #A90D91">var</span> <span style="color: #000000">thresholdKernel</span>: <span style="color: #5B269A">CIColorKernel</span> = { () -> <span style="color: #5B269A">CIColorKernel</span> <span style="color: #A90D91">in</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">url</span> = <span style="color: #000000">Bundle</span>.<span style="color: #000000">main</span>.<span style="color: #000000">url</span>(<span style="color: #000000">forResource</span>: <span style="color: #C41A16">"LumaThreshold"</span>, <span style="color: #000000">withExtension</span>: <span style="color: #C41A16">"ci.metallib"</span>)<span style="color: #000000">!</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">data</span> = <span style="color: #A90D91">try</span>! <span style="color: #000000">Data</span>(<span style="color: #000000">contentsOf</span>: <span style="color: #000000">url</span>)
<span style="color: #A90D91">do</span> {
<span style="color: #A90D91">return</span> <span style="color: #A90D91">try</span> <span style="color: #5B269A">CIColorKernel</span>(<span style="color: #000000">functionName</span>: <span style="color: #C41A16">"lumaThreshold"</span>, <span style="color: #000000">fromMetalLibraryData</span>: <span style="color: #000000">data</span>)
}
<span style="color: #A90D91">catch</span> {
<span style="color: #5B269A">print</span>(<span style="color: #C41A16">"\(</span><span style="color: #000000">error</span><span style="color: #C41A16">)"</span>)
<span style="color: #5B269A">fatalError</span>(<span style="color: #C41A16">"\(</span><span style="color: #000000">error</span><span style="color: #C41A16">)"</span>)
}
}()
</code></pre></div>
<p>If you were observant you might notice we made a <code>LumaThreshold.ci.metal</code> file in the source code, but are now loading a <code>LumaThreshold.ci.metallib</code> file at runtime.</p>
<p>When we try to build and run the app now we'll get a crash with the error message:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">> let url = </span>
<span style="color: #000000">^ MetaballEffectFilter.swift:56: Fatal error: Unexpectedly found nil while unwrapping an Optional value</span>
</code></pre></div>
<p>That's because first we need to preprocess the Metal files; unfortunately, Xcode doesn't do this automatically for you.</p>
<p>The WWDC 2020 video <a href="https://developer.apple.com/videos/play/wwdc2020/10021">Build Metal-based Core Image kernels with Xcode</a> shows you how to set up the needed custom build steps.<br />
However, if yo follow it exactly you will get more runtime errors, as the instructions are slightly wrong.</p>
<p>As of June 2022 using Xcode 13.4.1 these are the needed steps:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500"># First convert LumaThreshold.ci.metal to LumaThreshold.ci.air</span>
xcrun metal -c <span style="color: #000000">$MTL_HEADER_SEARCH_PATHS</span> -fcikernel <span style="color: #C41A16">"LumaThreshold.ci.metal"</span> -o <span style="color: #C41A16">"LumaThreshold.ci.air"</span>
<span style="color: #177500"># Then convert LumaThreshold.ci.air to LumaThreshold.ci.metallib</span>
xcrun metallib -cikernel <span style="color: #C41A16">"LumaThreshold.ci.air"</span> -o <span style="color: #C41A16">"LumaThreshold.ci.metallib"</span>
</code></pre></div>
<p>To do this automatically we'll setup some custom build rules like suggested in the WWDC video.</p>
<p>In the Xcode Project Settings > Build Rules tab, click on the + icon to setup a new build rule.</p>
<p><img src="https://ikyle.me/blog/2022/creating-a-coreimage-filter-with-a-metal-kernel/build-rules.png" alt="" /></p>
<p>Then create 2 new rules:</p>
<hr />
<p>First the one for <code>*.ci.metal</code> files.<br />
This is why we named it <code>.ci.metal</code>, instead of just <code>.metal</code>. So we can process this separately from any non CoreImage related Metal files we might have.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">Source files with names matching: *.ci.metal</span>
<span style="color: #000000">[ ] Run once per architecture</span>
</code></pre></div>
<p>Custom script:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>xcrun metal -c <span style="color: #000000">$MTL_HEADER_SEARCH_PATHS</span> -fcikernel <span style="color: #C41A16">"${</span><span style="color: #000000">INPUT_FILE_PATH</span><span style="color: #C41A16">}"</span> -o <span style="color: #C41A16">"${</span><span style="color: #000000">SCRIPT_OUTPUT_FILE_0</span><span style="color: #C41A16">}"</span>
</code></pre></div>
<p>Output Files</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE).air</span>
</code></pre></div>
<hr />
<p>Next step a step to turn these files into new <code>.air</code> files into <code>*.metallib</code>.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">Source files with names matching: *.ci.air</span>
<span style="color: #000000">[ ] Run once per architecture</span>
</code></pre></div>
<p>Custom script:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>xcrun metallib -cikernel <span style="color: #C41A16">"${</span><span style="color: #000000">INPUT_FILE_PATH</span><span style="color: #C41A16">}"</span> -o <span style="color: #C41A16">"${</span><span style="color: #000000">SCRIPT_OUTPUT_FILE_0</span><span style="color: #C41A16">}"</span>
</code></pre></div>
<p>Output Files</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">$(METAL_LIBRARY_OUTPUT_DIR)/$(INPUT_FILE_BASE).metallib</span>
</code></pre></div>
<hr />
<p><img src="https://ikyle.me/blog/2022/creating-a-coreimage-filter-with-a-metal-kernel/custom-build-rules.png" alt="" /></p>
<hr />
<blockquote>
<p>NOTE: The first above command is slightly different from the one in the WWWDC video.</p>
</blockquote>
<p>The WWDC video from 2020 shows this command:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>xcrun metal -c -I <span style="color: #000000">$MTL_HEADER_SEARCH_PATHS</span> -fcikernel <span style="color: #C41A16">"${</span><span style="color: #000000">INPUT_FILE_PATH</span><span style="color: #C41A16">}"</span> -o <span style="color: #C41A16">"${</span><span style="color: #000000">SCRIPT_OUTPUT_FILE_0</span><span style="color: #C41A16">}"</span>
</code></pre></div>
<p>If you use that however, it will strip out the function during linking and when you run the project you will get this error:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">MetaballEffectFilter.swift:64: Fatal error: Error Domain=CIKernel Code=1 "(null)" UserInfo={CINonLocalizedDescriptionKey=Function does not exist in library data. </span>
</code></pre></div>
<p>The problem is the <code>-I</code> remove that and your build scripts setup should work fine.</p>
<hr />
<p>If we build and run now, we should have a fully working CoreImage filter using a Metal Shader.</p>
<p><img src="https://ikyle.me/blog/2022/creating-a-coreimage-filter-with-a-metal-kernel/metaballs-close-small.png" alt="" /></p>
<h1>Useful Links</h1>
<ul>
<li><a href="https://vijaysharma.ca/creating-a-coreimage-filter-using-metal-kernels/">Creating a CoreImage Filter using Metal Kernels</a></li>
<li><a href="https://developer.apple.com/documentation/coreimage/writing_custom_kernels">Apple: Writing Custom Kernels</a></li>
<li><a href="https://pspdfkit.com/blog/2020/image-filters-using-metal/">PSPDFKIT: Using Metal to Apply Image Filters</a></li>
<li><a href="https://developer.apple.com/videos/play/wwdc2021/10159/">WWDC 2021: Explore Core Image kernel improvements</a></li>
<li><a href="https://developer.apple.com/videos/play/wwdc2020/10021/">WWDC 2020: Build Metal-based Core Image kernels with Xcode</a></li>
<li><a href="https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf">Apple: Metal Shading Language Specification</a></li>
</ul>
Creating Cool UI: iOS Shape Morphing
https://ikyle.me/blog/2022/creating-cool-ui-shape-morphing
2022-07-02T02:15:01.083349+00:00
2022-07-02T02:15:01.083349+00:00
Recreating cool UI demos I've seen online. This time, morphing between different icon shapes and learning about metaballs.
<p>The other day on Twitter I came across <a href="https://twitter.com/DLX/status/1541101687267594252">a tweet showing a really cool UI demo</a> and wondered how it was done.</p>
<p><a href="https://twitter.com/DLX/status/1541101687267594252"><img src="https://ikyle.me/blog/2022/creating-cool-ui-shape-morphing/twitter-example-video-480p.mp4" alt="" /></a></p>
<p>Seeing this tweet reminded me of an idea I've had for several years to do a blog post series showing how to actually create various cool UI concept videos, Dribbble posts, or weird UI elements from other apps.</p>
<p>So, as hopefully the first of many, let's see how to a Create Cool UI Component: "Shape Morphing" on iOS.</p>
<h2>Metaballs</h2>
<p>The technique for organic looking objects morphing between and into each other is known as <a href="https://en.wikipedia.org/wiki/Metaballs">metaballs</a>.</p>
<p>As two objects get close together, instead of waiting until they start to overlap.
<img src="https://ikyle.me/blog/2022/creating-cool-ui-shape-morphing/balls-close-small.png" alt="" /></p>
<p>The two objects will instead start stretching towards each other and combining.
<img src="https://ikyle.me/blog/2022/creating-cool-ui-shape-morphing/metaballs-close-small.png" alt="" /></p>
<h1>iOS Implementation</h1>
<p>To implement this effect we need to combine 2 different effects together (at least the way we are going to go about it, there are a few other ways to achieve this effect; which are linked at the bottom of the page).</p>
<p>We need to render our shapes and then apply these effects over the top as post processing steps to the rendered image.</p>
<p>CALayer's do have a <a href="https://developer.apple.com/documentation/quartzcore/calayer/1410901-filters"><code>.filters</code> property</a> which looks perfect for this!</p>
<blockquote>
<p>An array of Core Image filters to apply to the contents of the layer and its sublayers. Animatable</p>
</blockquote>
<p>Until you get to the bottom of the documentation.</p>
<blockquote>
<p>This property is not supported on layers in iOS</p>
</blockquote>
<p>🥲</p>
<p>Sadly such rendering effects aren't supported on iOS.<br />
So instead we'll need to use a rendering API which does let us post-process the rendered result.</p>
<p>On iOS we have the choice of either SpriteKit or SceneKit.</p>
<p>SceneKit has a cool trick of letting you set a UIView as the material of a node, which would be really helpful in applying the trick to UIView's as we could host the views inside the SceneKit view.</p>
<p>However, for now I'll just use SpriteKit as I don't need anything more for this example and it is simpler to work with.</p>
<h1>SpriteKit Implementation</h1>
<p>First we need to setup our scene:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">class</span> <span style="color: #3F6E75">SimpleViewController</span>: <span style="color: #5B269A">UIViewController</span> {
<span style="color: #177500">// Our SpriteKit Objects</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">skView</span> = <span style="color: #5B269A">SKView</span>()
<span style="color: #A90D91">let</span> <span style="color: #000000">scene</span> = <span style="color: #5B269A">SKScene</span>()
<span style="color: #177500">// The two balls</span>
<span style="color: #A90D91">var</span> <span style="color: #000000">blobOne</span>: <span style="color: #5B269A">SKShapeNode</span>? = <span style="color: #A90D91">nil</span>
<span style="color: #A90D91">var</span> <span style="color: #000000">blobTwo</span>: <span style="color: #5B269A">SKShapeNode</span>? = <span style="color: #A90D91">nil</span>
<span style="color: #A90D91">override</span> <span style="color: #A90D91">func</span> <span style="color: #000000">viewDidLoad</span>() {
<span style="color: #A90D91">super</span>.<span style="color: #000000">viewDidLoad</span>()
<span style="color: #177500">// Setup Scene</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">scene</span>.<span style="color: #000000">scaleMode</span> = .<span style="color: #000000">resizeFill</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">scene</span>.<span style="color: #000000">physicsWorld</span>.<span style="color: #000000">gravity</span> = <span style="color: #5B269A">CGVector</span>(<span style="color: #000000">dx</span>: <span style="color: #1C01CE">0</span>, <span style="color: #000000">dy</span>: <span style="color: #1C01CE">0</span>)
<span style="color: #A90D91">self</span>.<span style="color: #000000">skView</span>.<span style="color: #000000">presentScene</span>(<span style="color: #A90D91">self</span>.<span style="color: #000000">scene</span>)
<span style="color: #A90D91">self</span>.<span style="color: #000000">view</span>.<span style="color: #000000">addSubview</span>(<span style="color: #A90D91">self</span>.<span style="color: #000000">skView</span>);
<span style="color: #177500">// Create the 2 balls</span>
{
<span style="color: #A90D91">let</span> <span style="color: #000000">blob</span> = <span style="color: #5B269A">SKShapeNode</span>(<span style="color: #000000">circleOfRadius</span>: <span style="color: #1C01CE">50</span>)
<span style="color: #000000">blob</span>.<span style="color: #000000">fillColor</span> = <span style="color: #5B269A">UIColor</span>.<span style="color: #000000">yellow</span>
<span style="color: #000000">blob</span>.<span style="color: #000000">strokeColor</span> = <span style="color: #000000">blob</span>.<span style="color: #000000">fillColor</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">scene</span>.<span style="color: #000000">addChild</span>(<span style="color: #000000">blob</span>)
<span style="color: #A90D91">self</span>.<span style="color: #000000">blobOne</span> = <span style="color: #000000">blob</span>
}();
{
<span style="color: #A90D91">let</span> <span style="color: #000000">blob</span> = <span style="color: #5B269A">SKShapeNode</span>(<span style="color: #000000">circleOfRadius</span>: <span style="color: #1C01CE">50</span>)
<span style="color: #000000">blob</span>.<span style="color: #000000">fillColor</span> = <span style="color: #5B269A">UIColor</span>.<span style="color: #000000">white</span>
<span style="color: #000000">blob</span>.<span style="color: #000000">strokeColor</span> = <span style="color: #000000">blob</span>.<span style="color: #000000">fillColor</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">scene</span>.<span style="color: #000000">addChild</span>(<span style="color: #000000">blob</span>)
<span style="color: #A90D91">self</span>.<span style="color: #000000">blobTwo</span> = <span style="color: #000000">blob</span>
}();
}
<span style="color: #177500">// Set the positions of everything</span>
<span style="color: #A90D91">override</span> <span style="color: #A90D91">func</span> <span style="color: #000000">viewDidLayoutSubviews</span>() {
<span style="color: #A90D91">super</span>.<span style="color: #000000">viewDidLayoutSubviews</span>()
<span style="color: #A90D91">let</span> <span style="color: #000000">bounds</span> = <span style="color: #A90D91">self</span>.<span style="color: #000000">view</span>.<span style="color: #000000">bounds</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">skView</span>.<span style="color: #000000">frame</span> = <span style="color: #000000">bounds</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">center</span> = <span style="color: #5B269A">CGPoint</span>(<span style="color: #000000">x</span>: <span style="color: #000000">bounds</span>.<span style="color: #000000">midX</span>, <span style="color: #000000">y</span>: <span style="color: #000000">bounds</span>.<span style="color: #000000">midY</span>)
<span style="color: #A90D91">self</span>.<span style="color: #000000">blobOne</span>?.<span style="color: #000000">position</span> = <span style="color: #000000">center</span>.<span style="color: #000000">applying</span>(<span style="color: #5B269A">CGAffineTransform</span>(<span style="color: #000000">translationX</span>: <span style="color: #000000">-</span><span style="color: #1C01CE">50</span>, <span style="color: #000000">y</span>: <span style="color: #1C01CE">0</span>))
<span style="color: #A90D91">self</span>.<span style="color: #000000">blobTwo</span>?.<span style="color: #000000">position</span> = <span style="color: #000000">center</span>.<span style="color: #000000">applying</span>(<span style="color: #5B269A">CGAffineTransform</span>(<span style="color: #000000">translationX</span>: <span style="color: #1C01CE">50</span>, <span style="color: #000000">y</span>: <span style="color: #1C01CE">0</span>))
}
}
</code></pre></div>
<p>This gives us our basic scene. Two spheres near each other but only just touching.</p>
<p><img src="https://ikyle.me/blog/2022/creating-cool-ui-shape-morphing/two-metaballs-1-basic-small.png" alt="" /></p>
<p>Now let's add a blur effect at the end of <code>-viewDidLoad()</code>.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// - At the top of the file....</span>
<span style="color: #A90D91">import</span> <span style="color: #3F6E75">CoreImage</span>
<span style="color: #A90D91">import</span> <span style="color: #3F6E75">CoreImage</span>.<span style="color: #3F6E75">CIFilterBuiltins</span>
<span style="color: #177500">// - At the bottom of viewDidLoad</span>
<span style="color: #177500">// Create </span>
<span style="color: #A90D91">let</span> <span style="color: #000000">blur</span> = <span style="color: #5B269A">CIFilter</span>.<span style="color: #000000">gaussianBlur</span>()
<span style="color: #000000">blur</span>.<span style="color: #000000">radius</span> = <span style="color: #1C01CE">20</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">scene</span>.<span style="color: #5B269A">filter</span> = <span style="color: #000000">blur</span>
<span style="color: #177500">// Make sure the scene uses the filter we created</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">scene</span>.<span style="color: #000000">shouldEnableEffects</span> = <span style="color: #A90D91">true</span>
</code></pre></div>
<p><img src="https://ikyle.me/blog/2022/creating-cool-ui-shape-morphing/two-metaballs-2-blur-small.png" alt="" /></p>
<p>Now that we've done that we can see how the shapes are merged together as their blurs overlap and merge.</p>
<p>However, this still doesn't look like the effect we want. To achieve that we will need to apply a threshold filter where we make fully opaque everything over a certain threshold, and we hide every pixel below that.<br />
This will make sure our shapes still have precise boundaries and appear to merge, not just blur together.</p>
<p>We can't apply multiple filters to the SpriteKit scene, so we'll need to subclass <code>CIFilter</code> and make our own combined filter.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">class</span> <span style="color: #3F6E75">MetaballEffectFilter</span>: <span style="color: #5B269A">CIFilter</span>
{
<span style="color: #177500">// Internal Filters</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">blurFilter</span>: <span style="color: #5B269A">CIFilter</span> <span style="color: #000000">&</span> <span style="color: #000000">CIGaussianBlur</span> = {
<span style="color: #A90D91">let</span> <span style="color: #000000">blur</span> = <span style="color: #5B269A">CIFilter</span>.<span style="color: #000000">gaussianBlur</span>()
<span style="color: #000000">blur</span>.<span style="color: #000000">radius</span> = <span style="color: #1C01CE">30</span>
<span style="color: #A90D91">return</span> <span style="color: #000000">blur</span>
}()
<span style="color: #A90D91">let</span> <span style="color: #000000">thresholdFilter</span> = <span style="color: #000000">LumaThresholdFilter</span>()
<span style="color: #177500">// - CIFilter Subclass Properties</span>
<span style="color: #A90D91">@objc</span> <span style="color: #A90D91">dynamic</span> <span style="color: #A90D91">var</span> <span style="color: #000000">inputImage</span> : <span style="color: #5B269A">CIImage</span>?
<span style="color: #A90D91">override</span> <span style="color: #A90D91">var</span> <span style="color: #000000">outputImage</span>: <span style="color: #5B269A">CIImage</span>!
{
<span style="color: #A90D91">guard</span> <span style="color: #A90D91">let</span> <span style="color: #000000">inputImage</span> = <span style="color: #A90D91">self</span>.<span style="color: #000000">inputImage</span> <span style="color: #A90D91">else</span>
{
<span style="color: #A90D91">return</span> <span style="color: #A90D91">nil</span>
}
<span style="color: #177500">// Blur Image</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">blurFilter</span>.<span style="color: #000000">inputImage</span> = <span style="color: #000000">inputImage</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">blurredOutput</span> = <span style="color: #A90D91">self</span>.<span style="color: #000000">blurFilter</span>.<span style="color: #000000">outputImage</span>
<span style="color: #177500">// Clip to the threshold set</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">thresholdFilter</span>.<span style="color: #000000">inputImage</span> = <span style="color: #000000">blurredOutput</span>
<span style="color: #A90D91">return</span> <span style="color: #A90D91">self</span>.<span style="color: #000000">thresholdFilter</span>.<span style="color: #000000">outputImage</span>
}
}
</code></pre></div>
<p>First we blur the image, then clip the output to the threshold.<br />
But where does this <code>LumaThresholdFilter</code> come from? This is another class we'll need to make ourselves.</p>
<p>The easier way to do this is to use <code>Core Image Kernel Language API</code> which is deprecated. Instead you are meant to use the new metal shader CoreImage filter API.<br />
However, to make CoreImage metal shaders requires several more steps of tweaking your build config by adding custom build rules and build settings. So for now I'll show examples of both the metal and CoreImage shaders but will use the <code>Core Image Kernel Language</code> for the rest of the examples afterwards.</p>
<p>Metal Shader</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// LumaThreshold.ci.metal</span>
<span style="color: #633820">#include</span> <span style="color: #177500"><metal_stdlib></span><span style="color: #633820"></span>
<span style="color: #000000">using</span> <span style="color: #000000">namespace</span> <span style="color: #000000">metal</span>;
<span style="color: #633820">#include</span> <span style="color: #177500"><CoreImage/CoreImage.h></span><span style="color: #633820"></span>
<span style="color: #A90D91">extern</span> <span style="color: #C41A16">"C"</span> <span style="color: #000000">float4</span> <span style="color: #000000">lumaThreshold</span>(<span style="color: #000000">coreimage::sample_t</span> <span style="color: #000000">pixelColor</span>, <span style="color: #A90D91">float</span> <span style="color: #000000">threshold</span>, <span style="color: #000000">coreimage::destination</span> <span style="color: #000000">destination</span>)
{
<span style="color: #000000">float3</span> <span style="color: #000000">pixelRGB</span> <span style="color: #000000">=</span> <span style="color: #000000">pixelColor</span>.<span style="color: #000000">rgb</span>;
<span style="color: #A90D91">float</span> <span style="color: #000000">luma</span> <span style="color: #000000">=</span> (<span style="color: #000000">pixelRGB</span>.<span style="color: #000000">r</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">0.2126</span>) <span style="color: #000000">+</span> (<span style="color: #000000">pixelRGB</span>.<span style="color: #000000">g</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">0.7152</span>) <span style="color: #000000">+</span> (<span style="color: #000000">pixelRGB</span>.<span style="color: #000000">b</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">0.0722</span>);
<span style="color: #A90D91">return</span> (<span style="color: #000000">luma</span> <span style="color: #000000">></span> <span style="color: #000000">threshold</span>) <span style="color: #000000">?</span> <span style="color: #000000">float4</span>(<span style="color: #1C01CE">1.0</span>, <span style="color: #1C01CE">1.0</span>, <span style="color: #1C01CE">1.0</span>, <span style="color: #1C01CE">1.0</span>) <span style="color: #000000">:</span> <span style="color: #000000">float4</span>(<span style="color: #1C01CE">0.0</span>, <span style="color: #1C01CE">0.0</span>, <span style="color: #1C01CE">0.0</span>, <span style="color: #1C01CE">0.0</span>);
}
</code></pre></div>
<p>Core Image Kernel Language Shader</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// LumaThresholdFilter.swift</span>
<span style="color: #A90D91">class</span> <span style="color: #3F6E75">LumaThresholdFilter</span>: <span style="color: #5B269A">CIFilter</span>
{
<span style="color: #A90D91">var</span> <span style="color: #000000">threshold</span>: <span style="color: #000000">CGFloat</span> = <span style="color: #1C01CE">0.5</span>
<span style="color: #A90D91">static</span> <span style="color: #A90D91">let</span> <span style="color: #000000">thresholdKernel</span> = <span style="color: #5B269A">CIColorKernel</span>(<span style="color: #000000">source</span>:<span style="color: #C41A16">"""</span>
<span style="color: #C41A16">kernel vec4 thresholdFilter(__sample image, float threshold)</span>
<span style="color: #C41A16">{</span>
<span style="color: #C41A16"> float luma = (image.r * 0.2126) + (image.g * 0.7152) + (image.b * 0.0722);</span>
<span style="color: #C41A16"> return (luma > threshold) ? vec4(1.0, 1.0, 1.0, 1.0) : vec4(0.0, 0.0, 0.0, 0.0);</span>
<span style="color: #C41A16">}</span>
<span style="color: #C41A16">"""</span>)<span style="color: #000000">!</span>
<span style="color: #177500">//</span>
<span style="color: #A90D91">@objc</span> <span style="color: #A90D91">dynamic</span> <span style="color: #A90D91">var</span> <span style="color: #000000">inputImage</span> : <span style="color: #5B269A">CIImage</span>?
<span style="color: #A90D91">override</span> <span style="color: #A90D91">var</span> <span style="color: #000000">outputImage</span> : <span style="color: #5B269A">CIImage</span>!
{
<span style="color: #A90D91">guard</span> <span style="color: #A90D91">let</span> <span style="color: #000000">inputImage</span> = <span style="color: #A90D91">self</span>.<span style="color: #000000">inputImage</span> <span style="color: #A90D91">else</span>
{
<span style="color: #A90D91">return</span> <span style="color: #A90D91">nil</span>
}
<span style="color: #A90D91">let</span> <span style="color: #000000">arguments</span> = [<span style="color: #000000">inputImage</span>, <span style="color: #A90D91">Float</span>(<span style="color: #A90D91">self</span>.<span style="color: #000000">threshold</span>)] <span style="color: #A90D91">as</span> [<span style="color: #A90D91">Any</span>]
<span style="color: #A90D91">return</span> <span style="color: #A90D91">Self</span>.<span style="color: #000000">thresholdKernel</span>.<span style="color: #000000">apply</span>(<span style="color: #000000">extent</span>: <span style="color: #000000">inputImage</span>.<span style="color: #000000">extent</span>, <span style="color: #000000">arguments</span>: <span style="color: #000000">arguments</span>)
}
}
</code></pre></div>
<p>This now gives us our final effect:</p>
<p><img src="https://ikyle.me/blog/2022/creating-cool-ui-shape-morphing/two-metaballs-3-clamp-small.png" alt="" />
<img src="https://ikyle.me/blog/2022/creating-cool-ui-shape-morphing/metaball-bounce-loop-short.mp4" alt="" /></p>
<h1>From Metaballs to Icon Morphing</h1>
<p>Now we have our metaballs effect. But we still don't have the same icon morphing as shown in the original Twitter video. However, we now have everything we need.</p>
<p>Now we just need to fade in the new icon over the top of the old icon, while fading the old icon out.</p>
<p>First we need to swap the 2 circles for 1 icon. We'll also need to keep hold of the filter reference this time, so we can animate it.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">var</span> <span style="color: #000000">currentIcon</span>: <span style="color: #5B269A">SKSpriteNode</span>?
<span style="color: #A90D91">let</span> <span style="color: #000000">filter</span> = <span style="color: #000000">MetaballEffectFilter</span>()
</code></pre></div>
<p>And let's add some button on screen for us to use.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Property</span>
<span style="color: #A90D91">lazy</span> <span style="color: #A90D91">var</span> <span style="color: #000000">buttons</span> : [<span style="color: #5B269A">UIButton</span>] = { [<span style="color: #A90D91">unowned</span> <span style="color: #A90D91">self</span>] <span style="color: #A90D91">in</span>
<span style="color: #A90D91">return</span> [
<span style="color: #C41A16">"circle.fill"</span>,
<span style="color: #C41A16">"heart.fill"</span>,
<span style="color: #C41A16">"star.fill"</span>,
<span style="color: #C41A16">"bell.fill"</span>,
<span style="color: #C41A16">"bookmark.fill"</span>,
<span style="color: #C41A16">"tag.fill"</span>,
<span style="color: #C41A16">"bolt.fill"</span>,
<span style="color: #C41A16">"play.fill"</span>,
<span style="color: #C41A16">"pause.fill"</span>,
<span style="color: #C41A16">"squareshape.fill"</span>,
<span style="color: #C41A16">"key.fill"</span>,
<span style="color: #C41A16">"hexagon.fill"</span>,
<span style="color: #C41A16">"gearshape.fill"</span>,
<span style="color: #C41A16">"car.fill"</span>,
].<span style="color: #5B269A">map</span>({ <span style="color: #000000">buttonName</span> <span style="color: #A90D91">in</span>
<span style="color: #A90D91">var</span> <span style="color: #000000">config</span> = <span style="color: #5B269A">UIButton</span>.<span style="color: #000000">Configuration</span>.<span style="color: #000000">filled</span>()
<span style="color: #000000">config</span>.<span style="color: #000000">image</span> = <span style="color: #5B269A">UIImage</span>(<span style="color: #000000">systemName</span>: <span style="color: #000000">buttonName</span>)
<span style="color: #000000">config</span>.<span style="color: #000000">baseBackgroundColor</span> = <span style="color: #5B269A">UIColor</span>.<span style="color: #000000">white</span>
<span style="color: #000000">config</span>.<span style="color: #000000">baseForegroundColor</span> = <span style="color: #5B269A">UIColor</span>.<span style="color: #000000">black</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">button</span> = <span style="color: #5B269A">UIButton</span>(<span style="color: #000000">configuration</span>: <span style="color: #000000">config</span>)
<span style="color: #000000">button</span>.<span style="color: #000000">addAction</span>(<span style="color: #000000">UIAction</span>(<span style="color: #000000">handler</span>: { [<span style="color: #A90D91">weak</span> <span style="color: #A90D91">self</span>] <span style="color: #A90D91">_</span> <span style="color: #A90D91">in</span>
<span style="color: #A90D91">self</span>?.<span style="color: #000000">animateIconChange</span>(<span style="color: #000000">newIconName</span>: <span style="color: #000000">buttonName</span>, <span style="color: #000000">duration</span>: <span style="color: #1C01CE">0.5</span>)
}), <span style="color: #A90D91">for</span>: .<span style="color: #000000">touchUpInside</span>)
<span style="color: #A90D91">return</span> <span style="color: #000000">button</span>
})
}()
<span style="color: #177500">// ...in viewDidLoad()</span>
<span style="color: #A90D91">for</span> <span style="color: #000000">button</span> <span style="color: #A90D91">in</span> <span style="color: #A90D91">self</span>.<span style="color: #000000">buttons</span> {
<span style="color: #A90D91">self</span>.<span style="color: #000000">view</span>.<span style="color: #000000">addSubview</span>(<span style="color: #000000">button</span>)
}
<span style="color: #177500">// .. In viewDidLayoutSubviews()</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">buttonSize</span>: <span style="color: #5B269A">CGSize</span> = <span style="color: #5B269A">CGSize</span>(<span style="color: #000000">width</span>: <span style="color: #1C01CE">70</span>, <span style="color: #000000">height</span>: <span style="color: #1C01CE">50</span>)
<span style="color: #A90D91">var</span> <span style="color: #000000">x</span>: <span style="color: #000000">CGFloat</span> = <span style="color: #1C01CE">0</span>
<span style="color: #A90D91">var</span> <span style="color: #000000">y</span>: <span style="color: #000000">CGFloat</span> = <span style="color: #000000">bounds</span>.<span style="color: #000000">height</span> <span style="color: #000000">-</span> (<span style="color: #A90D91">self</span>.<span style="color: #000000">view</span>.<span style="color: #000000">safeAreaInsets</span>.<span style="color: #000000">bottom</span> <span style="color: #000000">+</span> <span style="color: #1C01CE">5</span> <span style="color: #000000">+</span> <span style="color: #000000">buttonSize</span>.<span style="color: #000000">height</span>)
<span style="color: #A90D91">for</span> <span style="color: #000000">button</span> <span style="color: #A90D91">in</span> <span style="color: #A90D91">self</span>.<span style="color: #000000">buttons</span> {
<span style="color: #000000">button</span>.<span style="color: #000000">bounds</span> = <span style="color: #5B269A">CGRect</span>(<span style="color: #000000">origin</span>: .<span style="color: #000000">zero</span>, <span style="color: #000000">size</span>: <span style="color: #000000">buttonSize</span>)
<span style="color: #000000">button</span>.<span style="color: #000000">center</span>.<span style="color: #000000">x</span> = <span style="color: #000000">x</span> <span style="color: #000000">+</span> (<span style="color: #000000">buttonSize</span>.<span style="color: #000000">width</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">0.5</span>)
<span style="color: #000000">button</span>.<span style="color: #000000">center</span>.<span style="color: #000000">y</span> = <span style="color: #000000">y</span> <span style="color: #000000">+</span> (<span style="color: #000000">buttonSize</span>.<span style="color: #000000">height</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">0.5</span>)
<span style="color: #000000">x</span> <span style="color: #000000">+=</span> <span style="color: #000000">buttonSize</span>.<span style="color: #000000">width</span> <span style="color: #000000">+</span> <span style="color: #1C01CE">5</span>
<span style="color: #A90D91">if</span> <span style="color: #000000">x</span> <span style="color: #000000">></span> <span style="color: #000000">bounds</span>.<span style="color: #000000">size</span>.<span style="color: #000000">width</span> <span style="color: #000000">-</span> (<span style="color: #000000">buttonSize</span>.<span style="color: #000000">width</span> <span style="color: #000000">+</span> <span style="color: #1C01CE">5</span>) {
<span style="color: #000000">x</span> = <span style="color: #1C01CE">0</span>
<span style="color: #000000">y</span> <span style="color: #000000">-=</span> <span style="color: #000000">buttonSize</span>.<span style="color: #000000">height</span> <span style="color: #000000">+</span> <span style="color: #1C01CE">5</span>
}
}
</code></pre></div>
<p>Now let's setup showing the icon. (Uses <a href="https://gist.github.com/kylehowells/27e6e1bf42f283d22c3ae8bb9faaeece">this UIImage helper</a>).</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// ...at the end of viewDidLoad()</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">animateIconChange</span>(<span style="color: #000000">newIconName</span>: <span style="color: #C41A16">"circle.fill"</span>, <span style="color: #000000">duration</span>: <span style="color: #1C01CE">0</span>)
<span style="color: #177500">// Create new method</span>
<span style="color: #A90D91">func</span> <span style="color: #000000">animateIconChange</span>(<span style="color: #000000">newIconName</span>: <span style="color: #A90D91">String</span>, <span style="color: #000000">duration</span>: <span style="color: #000000">CGFloat</span>, <span style="color: #000000">showPlayIcon</span>: <span style="color: #A90D91">Bool</span> = <span style="color: #A90D91">false</span>) {
<span style="color: #177500">// Create new icon shape</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">newIconShape</span>: <span style="color: #5B269A">SKSpriteNode</span>? = {
<span style="color: #A90D91">let</span> <span style="color: #000000">iconSize</span> = <span style="color: #5B269A">CGSize</span>(<span style="color: #000000">width</span>: <span style="color: #1C01CE">80</span>, <span style="color: #000000">height</span>: <span style="color: #1C01CE">80</span>)
<span style="color: #A90D91">guard</span> <span style="color: #A90D91">let</span> <span style="color: #000000">image</span> = <span style="color: #5B269A">UIImage</span>(<span style="color: #000000">systemName</span>: <span style="color: #000000">newIconName</span>)?.<span style="color: #000000">withTintColor</span>(<span style="color: #5B269A">UIColor</span>.<span style="color: #000000">white</span>).<span style="color: #000000">resized</span>(<span style="color: #000000">within</span>: <span style="color: #000000">iconSize</span>) <span style="color: #A90D91">else</span> { <span style="color: #A90D91">return</span> <span style="color: #A90D91">nil</span> }
<span style="color: #A90D91">let</span> <span style="color: #000000">texture</span> = <span style="color: #5B269A">SKTexture</span>(<span style="color: #000000">image</span>: <span style="color: #000000">image</span>)
<span style="color: #A90D91">let</span> <span style="color: #000000">sprite</span> = <span style="color: #5B269A">SKSpriteNode</span>(<span style="color: #000000">texture</span>: <span style="color: #000000">texture</span>, <span style="color: #000000">size</span>: <span style="color: #000000">iconSize</span>)
<span style="color: #A90D91">return</span> <span style="color: #000000">sprite</span>
}()
<span style="color: #000000">newIconShape</span>?.<span style="color: #000000">position</span> = <span style="color: #5B269A">CGPoint</span>(
<span style="color: #000000">x</span>: <span style="color: #A90D91">self</span>.<span style="color: #000000">view</span>.<span style="color: #000000">bounds</span>.<span style="color: #000000">midX</span>,
<span style="color: #000000">y</span>: <span style="color: #A90D91">self</span>.<span style="color: #000000">view</span>.<span style="color: #000000">bounds</span>.<span style="color: #000000">midY</span>
)
<span style="color: #000000">newIconShape</span>?.<span style="color: #000000">alpha</span> = <span style="color: #1C01CE">0</span>
<span style="color: #177500">// Add new icon</span>
<span style="color: #A90D91">if</span> <span style="color: #A90D91">let</span> <span style="color: #000000">newIconShape</span> = <span style="color: #000000">newIconShape</span> {
<span style="color: #A90D91">self</span>.<span style="color: #000000">scene</span>.<span style="color: #000000">addChild</span>(<span style="color: #000000">newIconShape</span>)
}
<span style="color: #A90D91">let</span> <span style="color: #000000">oldIconShape</span> = <span style="color: #A90D91">self</span>.<span style="color: #000000">currentIcon</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">currentIcon</span> = <span style="color: #A90D91">nil</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">currentIcon</span> = <span style="color: #000000">newIconShape</span>
<span style="color: #A90D91">if</span> <span style="color: #000000">duration</span> == <span style="color: #1C01CE">0</span> {
<span style="color: #000000">newIconShape</span>?.<span style="color: #000000">alpha</span> = <span style="color: #1C01CE">1</span>
<span style="color: #000000">oldIconShape</span>?.<span style="color: #000000">removeFromParent</span>()
<span style="color: #A90D91">return</span>
}
...
}
</code></pre></div>
<p>Now we have our basic structure.</p>
<p><img src="https://ikyle.me/blog/2022/creating-cool-ui-shape-morphing/icon-morph-demo-small.png" alt="" /></p>
<p>To animate the change similarly to how it appears on Twitter we want to do several things in quick succession:</p>
<ul>
<li>First animate the blur from 0 to something.</li>
<li>Halfway through that, start alpha fading in the new shape and out the old shape.</li>
<li>About 3/4rd way through the shapes fading from one to the other, then animate the blur back down to 0 so by the end you end up left with just the new target shape itself.</li>
</ul>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code> <span style="color: #177500">// ... continues</span>
<span style="color: #177500">// Animate the change</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">fadeDuration</span> = (<span style="color: #000000">duration</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">0.25</span>)
<span style="color: #177500">// Animate in the blur effect</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">animateBlur</span>(<span style="color: #000000">duration</span>: <span style="color: #000000">fadeDuration</span>, <span style="color: #000000">blur</span>: <span style="color: #1C01CE">5</span>, <span style="color: #000000">from</span>: <span style="color: #1C01CE">0</span>)
<span style="color: #177500">// Wait then start fading in the new icon and out the old</span>
<span style="color: #000000">DispatchQueue</span>.<span style="color: #000000">main</span>.<span style="color: #000000">asyncAfter</span>(<span style="color: #000000">deadline</span>: .<span style="color: #000000">now</span>() <span style="color: #000000">+</span> (<span style="color: #000000">fadeDuration</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">0.75</span>), <span style="color: #000000">execute</span>: {
<span style="color: #A90D91">let</span> <span style="color: #000000">swapDuration</span> = <span style="color: #000000">duration</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">0.5</span>
<span style="color: #000000">newIconShape</span>?.<span style="color: #000000">run</span>(<span style="color: #5B269A">SKAction</span>.<span style="color: #000000">fadeAlpha</span>(<span style="color: #000000">to</span>: <span style="color: #1C01CE">1</span>, <span style="color: #000000">duration</span>: <span style="color: #000000">swapDuration</span>))
<span style="color: #000000">oldIconShape</span>?.<span style="color: #000000">run</span>(<span style="color: #5B269A">SKAction</span>.<span style="color: #000000">fadeAlpha</span>(<span style="color: #000000">to</span>: <span style="color: #1C01CE">0</span>, <span style="color: #000000">duration</span>: <span style="color: #000000">swapDuration</span>))
<span style="color: #177500">// Wait, then start returning the view back to a non-blobby version</span>
<span style="color: #000000">DispatchQueue</span>.<span style="color: #000000">main</span>.<span style="color: #000000">asyncAfter</span>(<span style="color: #000000">deadline</span>: .<span style="color: #000000">now</span>() <span style="color: #000000">+</span> (<span style="color: #000000">swapDuration</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">0.75</span>), <span style="color: #000000">execute</span>: {
<span style="color: #A90D91">self</span>.<span style="color: #000000">animateBlur</span>(<span style="color: #000000">duration</span>: <span style="color: #000000">fadeDuration</span>, <span style="color: #000000">blur</span>: <span style="color: #1C01CE">0</span>, <span style="color: #000000">from</span>: <span style="color: #1C01CE">5</span>)
<span style="color: #177500">// Cleanup</span>
<span style="color: #000000">DispatchQueue</span>.<span style="color: #000000">main</span>.<span style="color: #000000">asyncAfter</span>(<span style="color: #000000">deadline</span>: .<span style="color: #000000">now</span>() <span style="color: #000000">+</span> <span style="color: #000000">fadeDuration</span>, <span style="color: #000000">execute</span>: {
<span style="color: #000000">oldIconShape</span>?.<span style="color: #000000">removeFromParent</span>()
})
})
})
}
<span style="color: #177500">// Helper which animates the blur</span>
<span style="color: #A90D91">func</span> <span style="color: #000000">animateBlur</span>(<span style="color: #000000">duration</span>: <span style="color: #000000">CGFloat</span>, <span style="color: #000000">blur</span> <span style="color: #000000">targetBlur</span>: <span style="color: #000000">CGFloat</span>, <span style="color: #000000">from</span>: <span style="color: #000000">CGFloat</span>) {
<span style="color: #A90D91">let</span> <span style="color: #000000">blurFade</span> = <span style="color: #5B269A">SKAction</span>.<span style="color: #000000">customAction</span>(<span style="color: #000000">withDuration</span>: <span style="color: #000000">duration</span>, <span style="color: #000000">actionBlock</span>: { (<span style="color: #000000">node</span>, <span style="color: #000000">elapsed</span>) <span style="color: #A90D91">in</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">percent</span> = <span style="color: #000000">elapsed</span> <span style="color: #000000">/</span> <span style="color: #000000">CGFloat</span>(<span style="color: #000000">duration</span>)
<span style="color: #A90D91">let</span> <span style="color: #000000">difference</span> = (<span style="color: #000000">targetBlur</span> <span style="color: #000000">-</span> <span style="color: #000000">from</span>)
<span style="color: #A90D91">let</span> <span style="color: #000000">currentBlur</span> = <span style="color: #000000">from</span> <span style="color: #000000">+</span> (<span style="color: #000000">difference</span> <span style="color: #000000">*</span> <span style="color: #000000">percent</span>)
<span style="color: #A90D91">self</span>.<span style="color: #5B269A">filter</span>.<span style="color: #000000">blurFilter</span>.<span style="color: #000000">setValue</span>(<span style="color: #000000">currentBlur</span>, <span style="color: #000000">forKey</span>: <span style="color: #000000">kCIInputRadiusKey</span>)
<span style="color: #A90D91">self</span>.<span style="color: #000000">scene</span>.<span style="color: #000000">shouldEnableEffects</span> = <span style="color: #A90D91">true</span>
})
<span style="color: #A90D91">self</span>.<span style="color: #000000">scene</span>.<span style="color: #000000">run</span>(<span style="color: #000000">blurFade</span>)
}
</code></pre></div>
<p>As the different parts of the shapes fade in and go over the threshold they appear.<br />
The blur also means the shapes sort of meld together as we sort earlier with the metaballs effect itself.</p>
<p>We basically fade from one icon to the other while animating the metaballs effect on and then back off again.</p>
<p>The result:</p>
<p><img src="https://ikyle.me/blog/2022/creating-cool-ui-shape-morphing/icon-morph-demo-short.mp4" alt="" /></p>
<p>You can change the feel of the effect by changing the speed and overlap of different parts of the animation; and how much blur you fade in during the animation.<br />
But I've found those values to look quite nice and match the style of the effect in the original video reasonably well.</p>
<h1>Useful Links</h1>
<ul>
<li><a href="https://www.gamedev.net/tutorials/_/technical/graphics-programming-and-theory/exploring-metaballs-and-isosurfaces-in-2d-r2556/">Exploring Metaballs and Isosurfaces in 2D</a></li>
<li><a href="https://blobs.webflow.io">Make and animate Metaballs with Webflow</a></li>
<li><a href="http://jamie-wong.com/2014/08/19/metaballs-and-marching-squares/">Metaballs and Marching Squares</a></li>
<li><a href="https://gist.github.com/robb/094f4f946d7f02c69b65bed19b0f25d9">GooeyView.swift</a></li>
<li><a href="https://github.com/rnkyr/metaballs">Blob effect implementation with UIKIt</a></li>
<li><a href="https://github.com/dabing1022/DBMetaballLoading">Metaball loading written in Swift</a></li>
<li><a href="https://github.com/FlexMonkey/Globular">Colourful SpriteKit Metaballs Controlled by 3D Touch</a></li>
</ul>
UITableViewCell Styles
https://ikyle.me/blog/2022/uitableviewcellstyles
2022-05-29T00:17:59.498373+00:00
2022-05-29T00:17:59.498361+00:00
Examples of each UITableViewCellStyle and UITableViewCellAccessoryType
<p>UITableView has various style options <code>UITableViewCell.CellStyle</code>, but the documentation doesn't include any images to show you what each one actually looks like.</p>
<h2>UITableView Cells</h2>
<p>When creating a table view cell there are 2 included style options <code>UITableViewCell.AccessoryType</code> and <code>UITableViewCell.CellStyle</code>.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">override</span> <span style="color: #A90D91">func</span> <span style="color: #000000">tableView</span>(<span style="color: #A90D91">_</span> <span style="color: #000000">tableView</span>: <span style="color: #5B269A">UITableView</span>, <span style="color: #000000">cellForRowAt</span> <span style="color: #000000">indexPath</span>: <span style="color: #000000">IndexPath</span>) -> <span style="color: #5B269A">UITableViewCell</span> {
<span style="color: #A90D91">let</span> <span style="color: #000000">reuseIdentifier</span> = <span style="color: #C41A16">"reuseIdentifier"</span>
<span style="color: #A90D91">var</span> <span style="color: #000000">cell</span>:<span style="color: #5B269A">UITableViewCell</span>? = <span style="color: #000000">tableView</span>.<span style="color: #000000">dequeueReusableCell</span>(<span style="color: #000000">withIdentifier</span>: <span style="color: #000000">reuseIdentifier</span>)
<span style="color: #A90D91">if</span> <span style="color: #000000">cell</span> == <span style="color: #A90D91">nil</span> {
<span style="color: #000000">cell</span> = <span style="color: #5B269A">UITableViewCell</span>(<span style="color: #000000">style</span>: <span style="color: #5B269A">UITableViewCell</span>.<span style="color: #000000">CellStyle</span>.<span style="color: #A90D91">default</span>, <span style="color: #000000">reuseIdentifier</span>: <span style="color: #000000">reuseIdentifier</span>)
}
<span style="color: #000000">cell</span>!.<span style="color: #000000">imageView</span>?.<span style="color: #000000">image</span> = <span style="color: #5B269A">UIImage</span>(<span style="color: #000000">named</span>: <span style="color: #C41A16">"exampleImage"</span>)
<span style="color: #000000">cell</span>!.<span style="color: #000000">textLabel</span>?.<span style="color: #000000">text</span> = <span style="color: #C41A16">"Text Label"</span>
<span style="color: #000000">cell</span>!.<span style="color: #000000">detailTextLabel</span>?.<span style="color: #000000">text</span> = <span style="color: #C41A16">"Detail Label"</span>
<span style="color: #000000">cell</span>!.<span style="color: #000000">accessoryType</span> = <span style="color: #5B269A">UITableViewCell</span>.<span style="color: #000000">AccessoryType</span>.<span style="color: #000000">disclosureIndicator</span>
<span style="color: #A90D91">return</span> <span style="color: #000000">cell</span>!
}
</code></pre></div>
<h2>UITableViewCellStyle</h2>
<table>
<thead>
<tr>
<th align="center">All Styles</th>
<th align="center">Including Images</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2022/uitableviewcellstyles/UITableViewCellStyle-style.png" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2022/uitableviewcellstyles/UITableViewCellStyle-styles_with_image.png" alt="" /></td>
</tr>
<tr>
<td align="center">All styles & accessories</td>
<td align="center">All styles, Images & accessories</td>
</tr>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2022/uitableviewcellstyles/UITableViewCellStyle-styles_without_image-AccessoryTypes.png" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2022/uitableviewcellstyles/UITableViewCellStyle-styles_with_image-AccessoryTypes.png" alt="" /></td>
</tr>
</tbody>
</table>
<h2>UITableViewCellAccessoryType</h2>
<table>
<thead>
<tr>
<th align="center">All Styles</th>
<th align="center">With Images</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2022/uitableviewcellstyles/UITableViewCellAccessoryType.png" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2022/uitableviewcellstyles/UITableViewCellAccessoryType_with_images.png" alt="" /></td>
</tr>
</tbody>
</table>
Understanding UIKit: CALayer Anchor Point
https://ikyle.me/blog/2022/understanding-uikit-calayer-anchor-point
2022-04-26T02:11:47.947967+00:00
2022-04-26T02:11:47.947967+00:00
Understanding how the CALayer.anchorPoint property effects layout
<p>All on screen elements (normally) on iOS are made of <code>UIView</code>'s. They have a frame, which have an <code>x</code> and <code>y</code> origin, and a <code>width</code> and <code>height</code> size.</p>
<p>The <code>frame</code> is actually a derived property. It comes from combining the <code>bounds</code>, <code>center</code> and <code>transform</code> properties. If you apply a <code>transform</code> to scale a view up by 2 its <code>frame</code> will grow, but its <code>bounds</code> will remain the same size.<br />
The bounds is the view's internal size, the frame is the external size.</p>
<h1>UIView Frame</h1>
<p>You can see in this example, applying the scaling changes the <code>frame</code>, but not the <code>bounds</code>.</p>
<p><img src="https://ikyle.me/blog/2022/understanding-uikit-calayer-anchor-point/AnchorPointDemo_frame.png" alt="2 example UIViews with the same bounds but different frames due to the transform" /></p>
<p>However, UIView's are actually a helper which adds touch and other event handling on top of the real on screen element. The real element which controls what appears on screen is the <code>CALayer</code> which backs each and every <code>UIView</code>.</p>
<p>CALayer's location on screen is controlled by very similar, but slightly different set of properties.<br />
The <code>bounds</code>, for internal size.<br />
The <code>position</code>, its location in its parent view.<br />
The <code>transform</code>, which is a 3D transform now (not just a 2D one like UIView's <code>transform</code> property)</p>
<p>And finally the <code>anchorPoint</code> property.</p>
<h1>CALayer anchorPoint</h1>
<p><img src="https://ikyle.me/blog/2022/understanding-uikit-calayer-anchor-point/AnchorPointDemo_Start.png" alt="3 example UIViews with different anchor points" /></p>
<p>By default <code>anchorPoint</code> is <code>(x: 0.5, y: 0.5)</code>. This is a relative value.<br />
Instead of being a point value in the parent view/layer like position or bounds, this is relative to its width and height.<br />
<code>(x:0, y:0)</code> is the top left corner.<br />
<code>(x:0.5, y:0.5)</code> the middle of the view.<br />
<code>(x:1, y: 1)</code> is the bottom right edge of the view.</p>
<p>The <code>anchorPoint</code> is the location inside the view/layer that the <code>UIView.center</code> or <code>CALayer.position</code> represents. Normally this defaults to the centre of the view.<br />
However, if you change the anchor point the entire layer shifts over based on this pivot point.</p>
<p><img src="https://ikyle.me/blog/2022/understanding-uikit-calayer-anchor-point/AnchorPointDemo_Normal.png" alt="3 example UIViews with different anchor points" /></p>
<p>The official documentation puts it nicely.</p>
<blockquote>
<p>All geometric manipulations to the view occur about the specified point. For example, applying a rotation transform to a layer with the default anchor point causes the layer to rotate around its center. Changing the anchor point to a different location would cause the layer to rotate around that new point.</p>
</blockquote>
<p>This hints at one of the big reasons you might want to use the anchorPoint property. It allows you to easily control how other properties, such as the transform (rotation and scaling) are applied to the views.</p>
<p>If we apply the same rotation to all the views from the above example you can see the different effects it has on each.</p>
<p><img src="https://ikyle.me/blog/2022/understanding-uikit-calayer-anchor-point/AnchorPointDemo_Rotation.png" alt="3 example UIViews with different anchor points with rotation applied" /></p>
<h1>Outside the Box</h1>
<p>All the examples above have the <code>anchorPoint</code> inside the view's <code>bounds</code>, as we would normally expect, with values between 0 and 1. However, there is nothing that requires that.<br />
We can freely set values above, or below, that range.</p>
<p>An X value of 1 is the full width away from the left edge. So a value of 2 would be 2 full widths of the layer away from the left edge.</p>
<p>Similarly a value of -1 would be 1 full width away from the left edge of the layer.</p>
<p>All the examples below have the same centre point set, but different anchor points (and some rotation).</p>
<p><img src="https://ikyle.me/blog/2022/understanding-uikit-calayer-anchor-point/AnchorPointDemo_out_of_the_box.png" alt="4 example UIViews with the same centre and size but different anchor points and rotation" /></p>
<p>Although these examples use rotation, the same also applies to scaling. Scaling up or down a layer is done from the point of origin of its <code>anchorPoint</code>.<br />
So if the anchorPoint is outside the bounds, when a layer is scaled up it will appear to expand away from the <code>anchorPoint</code>.</p>
UIBlurEffectStyles
https://ikyle.me/blog/2022/uiblureffectstyle
2022-04-01T00:58:43.764302+00:00
2022-04-01T00:58:43.764283+00:00
Examples of the different available UIBlurEffectStyle types
<p>Blurred backgrounds are a staple of modern app design, and the Apple provided UIView to achieve that effect with is via applying a <code>UIBlurEffect</code> to a <code>UIVisualEffectView</code> view.</p>
<h2>Example</h2>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">let</span> <span style="color: #000000">visualEffectView</span> = <span style="color: #5B269A">UIVisualEffectView</span>(<span style="color: #000000">effect</span>: <span style="color: #A90D91">nil</span>)
<span style="color: #A90D91">self</span>.<span style="color: #000000">view</span>.<span style="color: #000000">addSubview</span>(<span style="color: #000000">visualEffectView</span>)
<span style="color: #000000">visualEffectView</span>.<span style="color: #000000">effect</span> = <span style="color: #5B269A">UIBlurEffect</span>(<span style="color: #000000">style</span>: .<span style="color: #000000">systemMaterial</span>)
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #5B269A">UIVisualEffectView</span> <span style="color: #000000">*visualEffectView</span> <span style="color: #000000">=</span> [[<span style="color: #5B269A">UIVisualEffectView</span> <span style="color: #000000">alloc</span>] <span style="color: #000000">initWithEffect</span>:<span style="color: #A90D91">nil</span>];
[<span style="color: #A90D91">self</span>.<span style="color: #000000">view</span> <span style="color: #000000">addSubview</span>:<span style="color: #000000">visualEffectView</span>];
<span style="color: #000000">visualEffectView</span>.<span style="color: #000000">effect</span> <span style="color: #000000">=</span> [<span style="color: #5B269A">UIBlurEffect</span> <span style="color: #000000">effectWithStyle</span>:<span style="color: #000000">UIBlurEffectStyleSystemMaterial</span>];
</code></pre></div>
<h1>UIBlurEffectStyle</h1>
<p>The different blur effects available are:</p>
<ul>
<li>The original 4 from iOS 8</li>
</ul>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">UIBlurEffectStyleExtraLight</span>, <span style="color: #177500">// .extraLight</span>
<span style="color: #000000">UIBlurEffectStyleLight</span>, <span style="color: #177500">// .light</span>
<span style="color: #000000">UIBlurEffectStyleDark</span>, <span style="color: #177500">// .dark</span>
<span style="color: #000000">UIBlurEffectStyleExtraDark</span> <span style="color: #177500">// .extraDark</span>
</code></pre></div>
<ul>
<li>Adaptive Default Styles</li>
</ul>
<p>These will adapt to light and dark mode automatically as the user switches modes.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">UIBlurEffectStyleRegular</span>, <span style="color: #177500">// .regular</span>
<span style="color: #000000">UIBlurEffectStyleProminent</span> <span style="color: #177500">// .prominent</span>
</code></pre></div>
<ul>
<li>Adaptive Styles</li>
</ul>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">UIBlurEffectStyleSystemUltraThinMaterial</span> <span style="color: #177500">// .systemUltraThinMaterial</span>
<span style="color: #000000">UIBlurEffectStyleSystemThinMaterial</span> <span style="color: #177500">// .systemThinMaterial</span>
<span style="color: #000000">UIBlurEffectStyleSystemMaterial</span> <span style="color: #177500">// .system</span>
<span style="color: #000000">UIBlurEffectStyleSystemThickMaterial</span> <span style="color: #177500">// .systemThickMaterial</span>
<span style="color: #000000">UIBlurEffectStyleSystemChromeMaterial</span>. <span style="color: #177500">// .ystemChromeMaterial</span>
</code></pre></div>
<h1>Example Project</h1>
<p>To visualise these different styles I creates <a href="https://github.com/kylehowells/ikyle.me-code-examples/tree/master/Blur%20Effects%20ObjC">an example project</a> which allows you to see each style in light and dark mode.</p>
<table>
<thead>
<tr>
<th align="left"></th>
<th align="left"></th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/Basic_Test_App.jpg" alt="" /></td>
<td align="left"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/Options.jpg" alt="" /></td>
</tr>
</tbody>
</table>
<h1>Screenshots</h1>
<p>Examples of each UIBlurEffectStyle style</p>
<table>
<thead>
<tr>
<th align="center">UIBlurEffectStyle</th>
<th align="center">Screenshot</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">ExtraLight</td>
<td align="center">Light</td>
</tr>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/ExtraLight.jpeg" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/Light.jpeg" alt="" /></td>
</tr>
<tr>
<td align="center">Dark</td>
<td align="center">Regular</td>
</tr>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/Dark.jpeg" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/Regular.jpeg" alt="" /></td>
</tr>
<tr>
<td align="center">Prominent</td>
<td align="center">SystemUltraThinMaterial</td>
</tr>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/Prominent.jpeg" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemUltraThinMaterial.jpeg" alt="" /></td>
</tr>
<tr>
<td align="center">SystemThinMaterial</td>
<td align="center">SystemMaterial</td>
</tr>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemThinMaterial.jpeg" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemMaterial.jpeg" alt="" /></td>
</tr>
<tr>
<td align="center">SystemThickMaterial</td>
<td align="center">SystemChromeMaterial</td>
</tr>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemThickMaterial.jpeg" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemChromeMaterial.jpeg" alt="" /></td>
</tr>
<tr>
<td align="center">SystemUltraThinMaterialLight</td>
<td align="center">SystemThinMaterialLight</td>
</tr>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemUltraThinMaterialLight.jpeg" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemThinMaterialLight.jpeg" alt="" /></td>
</tr>
<tr>
<td align="center">SystemMaterialLight</td>
<td align="center">SystemThickMaterialLight</td>
</tr>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemMaterialLight.jpeg" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemThickMaterialLight.jpeg" alt="" /></td>
</tr>
<tr>
<td align="center">SystemChromeMaterialLight</td>
<td align="center">SystemUltraThinMaterialDark</td>
</tr>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemChromeMaterialLight.jpeg" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemUltraThinMaterialDark.jpeg" alt="" /></td>
</tr>
<tr>
<td align="center">SystemThinMaterialDark</td>
<td align="center">SystemMaterialDark</td>
</tr>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemThinMaterialDark.jpeg" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemMaterialDark.jpeg" alt="" /></td>
</tr>
<tr>
<td align="center">SystemThickMaterialDark</td>
<td align="center">SystemChromeMaterialDark</td>
</tr>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemThickMaterialDark.jpeg" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2022/uiblureffectstyle/SystemChromeMaterialDark.jpeg" alt="" /></td>
</tr>
</tbody>
</table>
Spherium 1.0 Released
https://ikyle.me/blog/2020/spherium-release
2020-08-13T14:33:58.135459+00:00
2020-08-13T14:33:58.135459+00:00
Spherium is designed to view and share 360 images in your Photo Library
<p><a href="https://spherium.app">Spherium</a> lets you easily view and share 360 degree images saved in your Photo Library.</p>
<p><img src="https://ikyle.me/blog/2020/spherium-release/scaled-iPad_Pro_(11-inch)_(2nd_generation)-landscape-4ShareSnapshot169_framed.png" alt="" /></p>
<p>Spherium will automatically attempt to find the 360º images saved in your photo library from 360º cameras, drones, apps such as Google Street View, or anywhere else you have saved them from.<br />
It looks for equirectangular images, images with a width equal to twice its height, with a minimum resolution of 1000 x 2000 pixels and lets you view and share those 360 photos.</p>
<p><img src="https://ikyle.me/blog/2020/spherium-release/iPadPro_(11-inch)_PhotoView.png" alt="" /></p>
<h1>Sharing</h1>
<p>Spherium supports taking and sharing normal FOV (field of view) snapshots of 360º images, as if you had taken a normal photo.<br />
This is really helpful for sharing the photos with friends and posting them on social media, like Instagram.</p>
<p>You can take one 360º photo and then take a portrait snapshot of it, for posting as an Instagram Story or WhatsApp status, and then also take a landscape or square snapshot for use in a normal Instagram post from the same photo.</p>
<p><img src="https://ikyle.me/blog/2020/spherium-release/iPadPro_(11-inch)_Share_Snapshot_9-16.png" alt="" /></p>
<h1>1.0 Released Now</h1>
<p>Spherium 1.0 is out now available on the iOS App Store.</p>
<ul>
<li><a href="https://apps.apple.com/us/app/id1502130060">App Store</a></li>
<li><a href="https://spherium.app">Offical Website</a></li>
<li><a href="https://spherium.app/faq/">FAQ</a></li>
<li><a href="https://spherium.app/presskit.zip">Download PressKit</a></li>
</ul>
<h1>Inspiration</h1>
<p>I love taking 360º group photos with friends, or of beautiful landscapes. Capturing the entire view all around me. Taking a photo, not just of me and my friends filling the cameras view, but also of our surroundings and the view all around us.</p>
<p>You can take 360º photos with dedicated cameras, such as the <a href="https://www.samsung.com/global/galaxy/gear-360/">Samsung Gear 360</a>, <a href="https://www.kandaovr.com/qoocam-8k/">Qoocam 8k</a>, <a href="https://www.insta360.com/product/insta360-oner_twin-edition/">Insta360 One R</a>, <a href="https://www.insta360.com/product/insta360-onex/">Insta360 One X</a>, <a href="https://gopro.com/en/us/shop/cameras/max/CHDHZ-201-master.html">Go Pro Fusion and GoPro Max</a>; drones some drones, such as the <a href="https://store.dji.com/guides/spark-sphere-mode-review/">DJI Mavic Air and Spark</a>; or even on your phone with apps, like <a href="https://apps.apple.com/gb/app/google-street-view/id904418768">Google's Street View app</a>.</p>
<p>360º cameras and drones usually have companion apps for the devices that support downloading and viewing the photos they take. The problem is then you end up with a photo collection split across various different apps, some in one app, some in another, and your normal photos in your phones Photo Library. Then there is also the risk the manufacture <a href="https://apps.apple.com/gb/app/samsung-gear-360/id1214791825">discontinues support for the app</a>.</p>
<p>The apps always have the ability to save the 360º photos to your Photo Library. Unfortunately iOS does not support viewing 360º photos natively, only showing the raw stretched image itself.</p>
<p><img src="https://ikyle.me/blog/2020/spherium-release/Country_Side.jpg" alt="" /></p>
<p>I wanted to be able to save all my photos, wherever they came from, in one place and view them all. That's why I built Spherium.</p>
<p><img src="https://ikyle.me/blog/2020/spherium-release/scaled-iPadPro_(11-inch)-Info.png" alt="" /></p>
<p><a href="https://apps.apple.com/us/app/id1502130060">Spherium 1.0 is available now</a></p>
How to Read and Write Image File Metadata with CoreGraphics
https://ikyle.me/blog/2020/ios-metadata-from-file
2020-07-13T23:33:52.630391+00:00
2020-07-13T23:33:52.630391+00:00
How to read and write Exif, GPS, IPTC, JFIF, TIFF and other metadata in image files using the CoreGraphics APIs on iOS and macOS
<p>UIImage allows you to very easily save the image to a file, with <a href="https://stackoverflow.com/questions/4623931/get-underlying-nsdata-from-uiimage"><code>UIImagePNGRepresentation(_)</code> or <code>UIImageJPEGRepresentation(_, _)</code></a>. However, those new images will be new, with the current date and time, and containing no location or camera metadata. If you want to edit an image without losing those things, that's a problem.</p>
<p>Fortunately, CoreGraphics includes several easy API's to access the image files raw metadata.<br />
However, these are the raw values themselves. If you want a CLLocation object from the GPS metadata you'll have to parse the <code>"{GPS}"</code> section of the metadata yourself.</p>
<h1>Read Image Metadata</h1>
<p>To read the image file metadata you only need a CoreGraphics image source and then to pass that image source to the <a href="https://developer.apple.com/documentation/imageio/1465363-cgimagesourcecopypropertiesatind"><code>CGImageSourceCopyPropertiesAtIndex(_, _, _)</code></a> function. This function then returns a <code>CFDictionaryRef</code>, which can just be cast to an <code>NSDictionary</code>, containing all the image file metadata.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">CGImageSourceRef</span> <span style="color: #000000">source</span> <span style="color: #000000">=</span> <span style="color: #000000">CGImageSourceCreateWithURL</span>((<span style="color: #A90D91">__bridge</span> <span style="color: #000000">CFURLRef</span>)<span style="color: #000000">url</span>, <span style="color: #A90D91">NULL</span>);
<span style="color: #000000">CFDictionaryRef</span> <span style="color: #000000">imageProperties</span> <span style="color: #000000">=</span> <span style="color: #000000">CGImageSourceCopyPropertiesAtIndex</span>(<span style="color: #000000">source</span>, <span style="color: #1C01CE">0</span>, <span style="color: #A90D91">NULL</span>);
<span style="color: #000000">NSLog</span>(<span style="color: #C41A16">@"%@"</span>, (<span style="color: #A90D91">__bridge</span> <span style="color: #5B269A">NSDictionary</span> <span style="color: #000000">*</span>)(<span style="color: #000000">imageProperties</span>) );
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">let</span> <span style="color: #000000">data</span> = <span style="color: #5B269A">NSData</span>(<span style="color: #000000">contentsOf</span>: <span style="color: #000000">url</span>)<span style="color: #000000">!</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">source</span> = <span style="color: #000000">CGImageSourceCreateWithData</span>(<span style="color: #000000">data</span> <span style="color: #A90D91">as</span> <span style="color: #000000">CFData</span>, <span style="color: #A90D91">nil</span>)<span style="color: #000000">!</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">metadata</span> = <span style="color: #000000">CGImageSourceCopyPropertiesAtIndex</span>(<span style="color: #000000">source</span>, <span style="color: #1C01CE">0</span>, <span style="color: #A90D91">nil</span>)<span style="color: #000000">!</span>
<span style="color: #5B269A">print</span>(<span style="color: #000000">metadata</span>)
</code></pre></div>
<h2>Example Image Metadata</h2>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">{</span>
<span style="color: #000000"> ColorModel = RGB;</span>
<span style="color: #000000"> Depth = 8;</span>
<span style="color: #000000"> PixelHeight = 5632;</span>
<span style="color: #000000"> PixelWidth = 11264;</span>
<span style="color: #000000"> ProfileName = "sRGB IEC61966-2.1";</span>
<span style="color: #000000"> "{Exif}" = {</span>
<span style="color: #000000"> ColorSpace = 1;</span>
<span style="color: #000000"> DateTimeDigitized = "2000:02:12 20:20:14";</span>
<span style="color: #000000"> DateTimeOriginal = "2000:02:12 20:20:14";</span>
<span style="color: #000000"> PixelXDimension = 11264;</span>
<span style="color: #000000"> PixelYDimension = 5632;</span>
<span style="color: #000000"> };</span>
<span style="color: #000000"> "{GPS}" = {</span>
<span style="color: #000000"> Altitude = "3.522659318754763";</span>
<span style="color: #000000"> AltitudeRef = 0;</span>
<span style="color: #000000"> Latitude = "50.79826333333333";</span>
<span style="color: #000000"> LatitudeRef = N;</span>
<span style="color: #000000"> Longitude = "0.5709116666666667";</span>
<span style="color: #000000"> LongitudeRef = W;</span>
<span style="color: #000000"> };</span>
<span style="color: #000000"> "{IPTC}" = {</span>
<span style="color: #000000"> DateCreated = 20000612;</span>
<span style="color: #000000"> DigitalCreationDate = 20000612;</span>
<span style="color: #000000"> DigitalCreationTime = 202014;</span>
<span style="color: #000000"> TimeCreated = 202014;</span>
<span style="color: #000000"> };</span>
<span style="color: #000000"> "{JFIF}" = {</span>
<span style="color: #000000"> DensityUnit = 0;</span>
<span style="color: #000000"> JFIFVersion = (</span>
<span style="color: #000000"> 1,</span>
<span style="color: #000000"> 0,</span>
<span style="color: #000000"> 1</span>
<span style="color: #000000"> );</span>
<span style="color: #000000"> XDensity = 72;</span>
<span style="color: #000000"> YDensity = 72;</span>
<span style="color: #000000"> };</span>
<span style="color: #000000"> "{TIFF}" = {</span>
<span style="color: #000000"> Make = Apple;</span>
<span style="color: #000000"> Model = "iPhone11,1 - 13.1.1";</span>
<span style="color: #000000"> };</span>
<span style="color: #000000">}</span>
<span style="color: #000000">{</span>
<span style="color: #000000"> "{Exif}" = {</span>
<span style="color: #000000"> DateTimeDigitized = "2010:01:12 20:20:14";</span>
<span style="color: #000000"> DateTimeOriginal = "2010:01:12 20:20:14";</span>
<span style="color: #000000"> };</span>
<span style="color: #000000"> "{GPS}" = {</span>
<span style="color: #000000"> Altitude = "2.522659318754763";</span>
<span style="color: #000000"> AltitudeRef = 0;</span>
<span style="color: #000000"> Latitude = "53.79826333333333";</span>
<span style="color: #000000"> LatitudeRef = N;</span>
<span style="color: #000000"> Longitude = "-1.5709116666666667";</span>
<span style="color: #000000"> LongitudeRef = W;</span>
<span style="color: #000000"> };</span>
<span style="color: #000000">}</span>
</code></pre></div>
<h1>Write Image Metadata</h1>
<p>To write image file metadata is very similar. Provided with an image source, and the type of image file being written to, we can create a new <code>CGImageDestinationRef</code> and then write in the image and metadata with <a href="https://developer.apple.com/documentation/imageio/1465143-cgimagedestinationaddimagefromso"><code>CGImageDestinationAddImageFromSource(_, _, _, _)</code></a>.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">CGImageSourceRef</span> <span style="color: #000000">source</span> <span style="color: #000000">=</span> <span style="color: #000000">CGImageSourceCreateWithURL</span>((<span style="color: #A90D91">__bridge</span> <span style="color: #000000">CFURLRef</span>)<span style="color: #000000">url</span>, <span style="color: #A90D91">NULL</span>);
<span style="color: #000000">CFStringRef</span> <span style="color: #000000">uniformTypeIdentifier</span> <span style="color: #000000">=</span> <span style="color: #000000">CGImageSourceGetType</span>(<span style="color: #000000">source</span>);
<span style="color: #000000">CGImageDestinationRef</span> <span style="color: #000000">destination</span> <span style="color: #000000">=</span> <span style="color: #000000">CGImageDestinationCreateWithURL</span>( (<span style="color: #A90D91">__bridge</span> <span style="color: #000000">CFURLRef</span>)<span style="color: #000000">url</span>, <span style="color: #000000">uniformTypeIdentifier</span>, <span style="color: #1C01CE">1</span>, <span style="color: #A90D91">NULL</span>);
<span style="color: #000000">CGImageDestinationAddImageFromSource</span>(<span style="color: #000000">destination</span>, <span style="color: #000000">source</span>, <span style="color: #1C01CE">0</span>, (<span style="color: #A90D91">__bridge</span> <span style="color: #000000">CFDictionaryRef</span>)<span style="color: #000000">metadata</span>);
<span style="color: #000000">CGImageDestinationFinalize</span>(<span style="color: #000000">destination</span>);
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">guard</span> <span style="color: #A90D91">let</span> <span style="color: #000000">source</span> = <span style="color: #000000">CGImageSourceCreateWithURL</span>(<span style="color: #000000">url</span> <span style="color: #A90D91">as</span> <span style="color: #000000">CFURL</span>, <span style="color: #A90D91">nil</span>),
<span style="color: #A90D91">let</span> <span style="color: #000000">uniformTypeIdentifier</span> = <span style="color: #000000">CGImageSourceGetType</span>(<span style="color: #000000">source</span>) <span style="color: #A90D91">else</span> { <span style="color: #A90D91">return</span> }
<span style="color: #A90D91">guard</span> <span style="color: #A90D91">let</span> <span style="color: #000000">destination</span> = <span style="color: #000000">CGImageDestinationCreateWithURL</span>(<span style="color: #000000">url</span> <span style="color: #A90D91">as</span> <span style="color: #000000">CFURL</span>, <span style="color: #000000">uniformTypeIdentifier</span>, <span style="color: #1C01CE">1</span>, <span style="color: #A90D91">nil</span>) <span style="color: #A90D91">else</span> { <span style="color: #A90D91">return</span> }
<span style="color: #000000">CGImageDestinationAddImageFromSource</span>(<span style="color: #000000">destination</span>, <span style="color: #000000">source</span>, <span style="color: #1C01CE">0</span>, <span style="color: #000000">metadata</span>)
<span style="color: #A90D91">guard</span> <span style="color: #000000">CGImageDestinationFinalize</span>(<span style="color: #000000">destination</span>) <span style="color: #A90D91">else</span> { <span style="color: #A90D91">return</span> }
</code></pre></div>
<h1>Example Projects</h1>
<p><img src="https://ikyle.me/blog/2020/ios-metadata-from-file/example_app.jpg" alt="Example project allowing editing of raw image file metadata" /></p>
<p>The example code Xcode Projects demonstrating how to use the new API are available on Github:</p>
<ul>
<li><a href="https://github.com/kylehowells/ikyle.me-code-examples/blob/master/Read%20Write%20Image%20Metadata%20ObjC/Read%20Write%20Image%20Metadata%20ObjC/ViewController.m#L126">ObjC Project</a></li>
<li><a href="https://github.com/kylehowells/ikyle.me-code-examples/blob/master/Read%20Write%20Image%20Metadata%20Swift/Read%20Write%20Image%20Metadata%20Swift/ViewController.swift#L89">Swift Project</a></li>
</ul>
<p>The example projects support iOS and macOS and allow you to select an image file. The XML representation of the metadata dictionary is then loaded into an editable text view. You can then save the edited metadata and export the resulting file.</p>
Using SwiftUI Previews with UIKit and ObjC
https://ikyle.me/blog/2020/swiftui-previews-uikit-objc
2020-07-01T23:34:35.165396+00:00
2020-07-01T23:34:35.165396+00:00
How to live(ish) preview UIKit UI written in Objective C
<p>One of the main selling points of SwiftUI is the live preview feature in Xcode. However, it is also possible to use this new feature with UIKit views, even ones written in Objective C.</p>
<p><img src="https://ikyle.me/blog/2020/swiftui-previews-uikit-objc/9-ObjC-Preview.png" alt="" /></p>
<h1>Xcode Preview Setup Steps</h1>
<p>Starting with a new Objective C Xcode project. Create a new <code>SwiftUI View</code> file.</p>
<p><img src="https://ikyle.me/blog/2020/swiftui-previews-uikit-objc/4-New-SwiftUI-File.mp4" alt="" /></p>
<ol>
<li>Accept the creation of the Objective-C bridging header.</li>
<li>Add your <code>UIViewController</code>'s to the bridging header.</li>
<li>Replace the <code>SwiftUI</code> view with a SwiftUI <code>UIViewController</code> wrapper.</li>
</ol>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">import</span> <span style="color: #3F6E75">SwiftUI</span>
<span style="color: #A90D91">struct</span> <span style="color: #3F6E75">VCRepresentable</span><<span style="color: #000000">VCClass</span>:<span style="color: #5B269A">UIViewController</span>>: <span style="color: #000000">UIViewControllerRepresentable</span> {
<span style="color: #A90D91">func</span> <span style="color: #000000">updateUIViewController</span>(<span style="color: #A90D91">_</span> <span style="color: #000000">uiViewController</span>: <span style="color: #5B269A">UIViewController</span>, <span style="color: #000000">context</span>: <span style="color: #000000">Context</span>) {
<span style="color: #177500">// leave this empty</span>
}
@<span style="color: #000000">available</span>(<span style="color: #000000">iOS</span> <span style="color: #1C01CE">13.0</span>.<span style="color: #1C01CE">0</span>, <span style="color: #000000">*</span>)
<span style="color: #A90D91">func</span> <span style="color: #000000">makeUIViewController</span>(<span style="color: #000000">context</span>: <span style="color: #000000">Context</span>) -> <span style="color: #5B269A">UIViewController</span> {
<span style="color: #A90D91">return</span> <span style="color: #000000">VCClass</span>()
}
}
<span style="color: #A90D91">struct</span> <span style="color: #3F6E75">SwiftUIView_Previews</span>: <span style="color: #000000">PreviewProvider</span> {
<span style="color: #A90D91">static</span> <span style="color: #A90D91">var</span> <span style="color: #000000">previews</span>: <span style="color: #000000">some</span> <span style="color: #000000">View</span> {
<span style="color: #000000">VCRepresentable</span><<span style="color: #000000">ViewController</span>>()
}
}
</code></pre></div>
<p>Xcode won't auto-refresh any changes made outside of the <code>body</code> method, so to refresh you need to press the <code>Resume</code> button or use the keyboard shortcut <code>CMD</code>+<code>ALT</code>+<code>P</code>.</p>
<p>Xcode takes a shortcut to reload a SwiftUI view, but needs to do a rebuild for changes made outside of the views body method (like changes to the ObjC file). Fortunately, if you are using ObjC, the rebuild will be fast so that doesn't matter very much.</p>
<p><img src="https://ikyle.me/blog/2020/swiftui-previews-uikit-objc/1-ObjC-UI-Preview.mp4" alt="" /></p>
<p>For more information on using Xcode Preview's with UIKit in general <a href="https://twitter.com/soulchildpls/status/1269912777772003328">@soulchildpls</a>'s "<a href="https://fluffy.es/xcode-previews-uikit/">Use Xcode Previews with UIKit</a>" article goes into more details.</p>
<h2>Example Project</h2>
<ul>
<li><a href="https://github.com/kylehowells/ikyle.me-code-examples/tree/master/ObjC%20Live%20Preview">Objective C Example Project</a></li>
</ul>
SwiftUI Gradient Text
https://ikyle.me/blog/2020/swiftui-gradient-text
2020-06-27T23:04:00.593509+00:00
2020-06-27T23:04:00.593509+00:00
How to have text with a gradient fill in SwiftUI
<p>When searching how to implement gradient text with SwiftUI the solution I found was to use a <code>LinearGradient</code> and use the <code>Text</code> element as the mask for that <code>View</code>. However, <code>LinearGradient</code> wants to expand to fill all the available space, and all the examples I found used fixed size <code>View</code>s.</p>
<p>The implementation I came up with to implement this for arbitrary fonts and text was to:</p>
<ul>
<li>Create a transparent text, to set the needed size.</li>
<li>Put the <code>LinearGradient</code> into the text's <code>background</code> property.</li>
<li>Place a second, duplicate, <code>Text</code> element as the mask for the gradient.</li>
</ul>
<p><img src="https://ikyle.me/blog/2020/swiftui-gradient-text/gradient_text.png" alt="" /></p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Custom View</span>
<span style="color: #A90D91">struct</span> <span style="color: #3F6E75">GradientText</span>: <span style="color: #000000">View</span> {
@<span style="color: #000000">State</span> <span style="color: #A90D91">var</span> <span style="color: #000000">text</span>: <span style="color: #A90D91">String</span>
@<span style="color: #000000">State</span> <span style="color: #A90D91">var</span> <span style="color: #000000">gradient</span>:<span style="color: #000000">LinearGradient</span> = <span style="color: #000000">LinearGradient</span>(
<span style="color: #000000">gradient</span>: <span style="color: #000000">Gradient</span>(<span style="color: #000000">colors</span>: [.<span style="color: #000000">white</span>, .<span style="color: #000000">gray</span>]),
<span style="color: #000000">startPoint</span>: .<span style="color: #000000">top</span>,
<span style="color: #000000">endPoint</span>: .<span style="color: #000000">bottom</span>
)
<span style="color: #A90D91">var</span> <span style="color: #000000">body</span>: <span style="color: #000000">some</span> <span style="color: #000000">View</span> {
<span style="color: #000000">Text</span>(<span style="color: #000000">text</span>)
.<span style="color: #000000">foregroundColor</span>(<span style="color: #000000">Color</span>.<span style="color: #000000">clear</span>)
.<span style="color: #000000">background</span>(<span style="color: #000000">gradient</span>.<span style="color: #000000">mask</span>(<span style="color: #000000">Text</span>(<span style="color: #A90D91">self</span>.<span style="color: #000000">text</span>)))
}
}
<span style="color: #177500">// Example Usage</span>
<span style="color: #A90D91">struct</span> <span style="color: #3F6E75">Gradient_Previews</span>: <span style="color: #000000">PreviewProvider</span> {
<span style="color: #A90D91">static</span> <span style="color: #A90D91">var</span> <span style="color: #000000">previews</span>: <span style="color: #000000">some</span> <span style="color: #000000">View</span> {
<span style="color: #000000">GradientText</span>(<span style="color: #000000">text</span>: <span style="color: #C41A16">"Hello World Gradient Text!"</span>)
.<span style="color: #000000">font</span>(.<span style="color: #000000">system</span>(<span style="color: #000000">size</span>: <span style="color: #1C01CE">50</span>, <span style="color: #000000">weight</span>: .<span style="color: #000000">bold</span>))
.<span style="color: #000000">padding</span>()
.<span style="color: #000000">background</span>(<span style="color: #000000">Color</span>.<span style="color: #000000">black</span>)
.<span style="color: #000000">previewLayout</span>(<span style="color: #000000">PreviewLayout</span>.<span style="color: #000000">sizeThatFits</span>)
.<span style="color: #000000">padding</span>()
.<span style="color: #000000">previewDisplayName</span>(<span style="color: #C41A16">"Example Gradient Text"</span>)
}
}
</code></pre></div>
<p>If anyone knows a better solution without the duplicate <code>Text</code> element please let me know on <a href="https://twitter.com/Freerunnering">Twitter</a> or <a href="https://mastodon.technology/@ikyle">Mastodon</a>.</p>
Using PHPickerViewController to Select a Photo on iOS 14
https://ikyle.me/blog/2020/phpickerviewcontroller
2020-06-23T03:59:40.595178+00:00
2020-06-23T03:59:40.595178+00:00
How to use the new PHPickerViewController class in iOS 14 to select a photo from the Photo Library on iOS in Objective C and Swift
<p>For many years the simplest way to selection photos and videos on iOS has been to use the <code>UIImagePickerController</code> class. The class allowed you to present a built in system UI to select a photo or video and return it to your app, without having to built the selection UI or prompt for access to the photo library.</p>
<p>The code to do so was incredibly simple:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Objective C</span>
- (<span style="color: #A90D91">void</span>)<span style="color: #000000">pickPhoto</span>
{
<span style="color: #5B269A">UIImagePickerController</span> <span style="color: #000000">*imagePicker</span> <span style="color: #000000">=</span> [[<span style="color: #5B269A">UIImagePickerController</span> <span style="color: #000000">alloc</span>] <span style="color: #000000">init</span>];
<span style="color: #000000">imagePicker</span>.<span style="color: #000000">sourceType</span> <span style="color: #000000">=</span> <span style="color: #000000">UIImagePickerControllerSourceTypePhotoLibrary</span>;
<span style="color: #000000">imagePicker</span>.<span style="color: #000000">delegate</span> <span style="color: #000000">=</span> <span style="color: #A90D91">self</span>;
[<span style="color: #A90D91">self</span> <span style="color: #000000">presentViewController</span>:<span style="color: #000000">imagePicker</span> <span style="color: #000000">animated</span>:<span style="color: #A90D91">YES</span> <span style="color: #000000">completion</span>:<span style="color: #A90D91">nil</span>];
}
<span style="color: #177500">// Implement UIImagePickerControllerDelegate method</span>
-(<span style="color: #A90D91">void</span>)<span style="color: #000000">imagePickerController:</span>(<span style="color: #5B269A">UIImagePickerController</span> <span style="color: #000000">*</span>)<span style="color: #000000">picker</span> <span style="color: #000000">didFinishPickingMediaWithInfo:</span>(<span style="color: #5B269A">NSDictionary</span><span style="color: #000000"><UIImagePickerControllerInfoKey</span>,<span style="color: #A90D91">id</span><span style="color: #000000">></span> <span style="color: #000000">*</span>)<span style="color: #000000">info</span>
{
<span style="color: #5B269A">UIImage</span> <span style="color: #000000">*image</span> <span style="color: #000000">=</span> <span style="color: #000000">info</span>[<span style="color: #000000">UIImagePickerControllerOriginalImage</span>];
<span style="color: #A90D91">self</span>.<span style="color: #000000">imageView</span>.<span style="color: #000000">image</span> <span style="color: #000000">=</span> <span style="color: #000000">image</span>;
[<span style="color: #000000">picker</span> <span style="color: #000000">dismissViewControllerAnimated</span>:<span style="color: #A90D91">YES</span> <span style="color: #000000">completion</span>:<span style="color: #A90D91">nil</span>];
}
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Swift</span>
<span style="color: #A90D91">func</span> <span style="color: #000000">pickPhoto</span>()
{
<span style="color: #A90D91">let</span> <span style="color: #000000">imagePicker</span> = <span style="color: #5B269A">UIImagePickerController</span>()
<span style="color: #000000">imagePicker</span>.<span style="color: #000000">sourceType</span> = .<span style="color: #000000">photoLibrary</span>
<span style="color: #000000">imagePicker</span>.<span style="color: #000000">delegate</span> = <span style="color: #A90D91">self</span>
<span style="color: #000000">present</span>(<span style="color: #000000">imagePicker</span>, <span style="color: #000000">animated</span>: <span style="color: #A90D91">true</span>)
}
<span style="color: #177500">// Implement UIImagePickerControllerDelegate method</span>
<span style="color: #A90D91">public</span> <span style="color: #A90D91">func</span> <span style="color: #000000">imagePickerController</span>(<span style="color: #A90D91">_</span> <span style="color: #000000">picker</span>: <span style="color: #5B269A">UIImagePickerController</span>, <span style="color: #000000">didFinishPickingMediaWithInfo</span> <span style="color: #000000">info</span>: [<span style="color: #5B269A">UIImagePickerController</span>.<span style="color: #000000">InfoKey</span>: <span style="color: #A90D91">Any</span>])
{
<span style="color: #000000">imageView</span>.<span style="color: #000000">image</span> = (<span style="color: #000000">info</span>[.<span style="color: #000000">originalImage</span>] <span style="color: #A90D91">as</span>? <span style="color: #5B269A">UIImage</span>)
<span style="color: #000000">picker</span>.<span style="color: #000000">dismiss</span>(<span style="color: #000000">animated</span>: <span style="color: #A90D91">true</span>, <span style="color: #000000">completion</span>: <span style="color: #A90D91">nil</span>)
}
</code></pre></div>
<p>However, <code>UIImagePickerController</code> had a number of disadvantages: it was fairly basic and limited in the UI it gave the users to browse their library with; it only allowed selection of one item at a time, and had only basic filtering support. Now in iOS 14 the <code>UIImagePickerController</code> is "soft deprecated". Although not currently marked as deprecated, if you look at the header file you'll see the API marker with this:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">API_DEPRECATED("Will be removed in a future release, use PHPicker.", ios(11, API_TO_BE_DEPRECATED));</span>
</code></pre></div>
<p><code>API_TO_BE_DEPRECATED</code> is defined with this comment above it.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">/* API_TO_BE_DEPRECATED is used as a version number in API that will be deprecated </span>
<span style="color: #000000"> * in an upcoming release. This soft deprecation is an intermediate step before formal </span>
<span style="color: #000000"> * deprecation to notify developers about the API before compiler warnings are generated.</span>
<span style="color: #000000">...</span>
</code></pre></div>
<p>Instead iOS 14 now includes the <code>PHPickerViewController</code> with support for multiple selection and a much improved UI.</p>
<h1>PHPicker</h1>
<p>Rather than being in UIKit, the new <code>PHPicker.</code> classes in iOS 14 are located in the <code>PhotosUI</code> framework and include:</p>
<ul>
<li><a href="https://developer.apple.com/documentation/photokit/phpickerviewcontroller"><code>PHPickerViewController</code></a></li>
<li><a href="https://developer.apple.com/documentation/photokit/phpickerconfiguration"><code>PHPickerConfiguration</code></a></li>
<li><a href="https://developer.apple.com/documentation/photokit/phpickerfilter"><code>PHPickerFilter</code></a></li>
<li><a href="https://developer.apple.com/documentation/photokit/phpickerresult"><code>PHPickerResult</code></a></li>
</ul>
<p>You present a <code>PHPickerViewController</code>, which has a <code>PHPickerConfiguration</code> to tell it how many items to select, and of what type. The types of items allowed are defined by the <code>PHPickerConfiguration</code>'s <code>filter</code> property, which has the possible option of being any combination of: images, live photos, or videos. How many items users can select is controlled from the <code>PHPickerConfiguration</code>'s <code>selectionLimit</code> property.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">PHPickerConfiguration</span> <span style="color: #000000">*config</span> <span style="color: #000000">=</span> [[<span style="color: #000000">PHPickerConfiguration</span> <span style="color: #000000">alloc</span>] <span style="color: #000000">init</span>];
<span style="color: #000000">config</span>.<span style="color: #000000">selectionLimit</span> <span style="color: #000000">=</span> <span style="color: #1C01CE">3</span>;
<span style="color: #000000">config</span>.<span style="color: #000000">filter</span> <span style="color: #000000">=</span> [<span style="color: #000000">PHPickerFilter</span> <span style="color: #000000">imagesFilter</span>];
<span style="color: #000000">PHPickerViewController</span> <span style="color: #000000">*pickerViewController</span> <span style="color: #000000">=</span> [[<span style="color: #000000">PHPickerViewController</span> <span style="color: #000000">alloc</span>] <span style="color: #000000">initWithConfiguration</span>:<span style="color: #000000">config</span>];
<span style="color: #000000">pickerViewController</span>.<span style="color: #000000">delegate</span> <span style="color: #000000">=</span> <span style="color: #A90D91">self</span>;
[<span style="color: #A90D91">self</span> <span style="color: #000000">presentViewController</span>:<span style="color: #000000">pickerViewController</span> <span style="color: #000000">animated</span>:<span style="color: #A90D91">YES</span> <span style="color: #000000">completion</span>:<span style="color: #A90D91">nil</span>];
</code></pre></div>
<p><code>PHPickerConfiguration</code>, <code>PHPickerFilter</code>, and <code>PHPickerResult</code> all bridge across into Swift as Structs, not as classes.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">var</span> <span style="color: #000000">config</span> = <span style="color: #000000">PHPickerConfiguration</span>()
<span style="color: #000000">config</span>.<span style="color: #000000">selectionLimit</span> = <span style="color: #1C01CE">3</span>
<span style="color: #000000">config</span>.<span style="color: #5B269A">filter</span> = <span style="color: #000000">PHPickerFilter</span>.<span style="color: #000000">images</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">pickerViewController</span> = <span style="color: #000000">PHPickerViewController</span>(<span style="color: #000000">configuration</span>: <span style="color: #000000">config</span>)
<span style="color: #000000">pickerViewController</span>.<span style="color: #000000">delegate</span> = <span style="color: #A90D91">self</span>
<span style="color: #A90D91">self</span>.<span style="color: #000000">present</span>(<span style="color: #000000">pickerViewController</span>, <span style="color: #000000">animated</span>: <span style="color: #A90D91">true</span>, <span style="color: #000000">completion</span>: <span style="color: #A90D91">nil</span>)
</code></pre></div>
<p>The results from <code>PHPickerViewController</code> are returned differently than <code>UIImagePickerController</code>, instead using <a href="https://developer.apple.com/documentation/foundation/nsitemprovider"><code>NSItemProvider</code></a>, like the <a href="https://developer.apple.com/documentation/uikit/drag_and_drop">drag and drop APIs</a>.</p>
<p><strong>NOTE:</strong> There is an <code>assetIdentifier</code> property defined on <code>PHPickerResult</code>, which is documented as <code>Local identifier of the selected asset</code>. However, (as of iOS 14.0 beta 1, in my testing this is always nil; regardless of whether or not the app has been granted full photo library access.<br />
I presume that is meant to be the <a href="https://developer.apple.com/documentation/photokit/phasset"><code>PHAsset</code></a>'s <code>assetIdentifier</code> allowing us to retrieve the asset object with a call to <code>asset = PHAsset.fetchAssets(withLocalIdentifiers: [photoID], options: nil).firstObject</code>.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Objective C</span>
-(<span style="color: #A90D91">void</span>)<span style="color: #000000">picker:</span>(<span style="color: #000000">PHPickerViewController</span> <span style="color: #000000">*</span>)<span style="color: #000000">picker</span> <span style="color: #000000">didFinishPicking:</span>(<span style="color: #5B269A">NSArray</span><span style="color: #000000"><PHPickerResult</span> <span style="color: #000000">*></span> <span style="color: #000000">*</span>)<span style="color: #000000">results</span>{
[<span style="color: #000000">picker</span> <span style="color: #000000">dismissViewControllerAnimated</span>:<span style="color: #A90D91">YES</span> <span style="color: #000000">completion</span>:<span style="color: #A90D91">nil</span>];
<span style="color: #A90D91">for</span> (<span style="color: #000000">PHPickerResult</span> <span style="color: #000000">*result</span> <span style="color: #A90D91">in</span> <span style="color: #000000">results</span>)
{
<span style="color: #177500">// Get UIImage</span>
[<span style="color: #000000">result</span>.<span style="color: #000000">itemProvider</span> <span style="color: #000000">loadObjectOfClass</span>:[<span style="color: #5B269A">UIImage</span> <span style="color: #A90D91">class</span>] <span style="color: #000000">completionHandler</span>:<span style="color: #000000">^</span>(<span style="color: #000000">__kindof</span> <span style="color: #A90D91">id</span><span style="color: #000000"><NSItemProviderReading></span> <span style="color: #000000">_Nullable</span> <span style="color: #000000">object</span>, <span style="color: #5B269A">NSError</span> <span style="color: #000000">*</span> <span style="color: #000000">_Nullable</span> <span style="color: #000000">error</span>)
{
<span style="color: #A90D91">if</span> ([<span style="color: #000000">object</span> <span style="color: #000000">isKindOfClass</span>:[<span style="color: #5B269A">UIImage</span> <span style="color: #A90D91">class</span>]])
{
<span style="color: #000000">dispatch_async</span>(<span style="color: #000000">dispatch_get_main_queue</span>(), <span style="color: #000000">^</span>{
<span style="color: #000000">NSLog</span>(<span style="color: #C41A16">@"Selected image: %@"</span>, (<span style="color: #5B269A">UIImage</span><span style="color: #000000">*</span>)<span style="color: #000000">object</span>);
});
}
}];
}
}
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Swift</span>
<span style="color: #A90D91">func</span> <span style="color: #000000">picker</span>(<span style="color: #A90D91">_</span> <span style="color: #000000">picker</span>: <span style="color: #000000">PHPickerViewController</span>, <span style="color: #000000">didFinishPicking</span> <span style="color: #000000">results</span>: [<span style="color: #000000">PHPickerResult</span>]) {
<span style="color: #000000">picker</span>.<span style="color: #000000">dismiss</span>(<span style="color: #000000">animated</span>: <span style="color: #A90D91">true</span>, <span style="color: #000000">completion</span>: <span style="color: #A90D91">nil</span>)
<span style="color: #A90D91">for</span> <span style="color: #000000">result</span> <span style="color: #A90D91">in</span> <span style="color: #000000">results</span> {
<span style="color: #000000">result</span>.<span style="color: #000000">itemProvider</span>.<span style="color: #000000">loadObject</span>(<span style="color: #000000">ofClass</span>: <span style="color: #5B269A">UIImage</span>.<span style="color: #A90D91">self</span>, <span style="color: #000000">completionHandler</span>: { (<span style="color: #000000">object</span>, <span style="color: #000000">error</span>) <span style="color: #A90D91">in</span>
<span style="color: #A90D91">if</span> <span style="color: #A90D91">let</span> <span style="color: #000000">image</span> = <span style="color: #000000">object</span> <span style="color: #A90D91">as</span>? <span style="color: #5B269A">UIImage</span> {
<span style="color: #000000">DispatchQueue</span>.<span style="color: #000000">main</span>.<span style="color: #000000">async</span> {
<span style="color: #177500">// Use UIImage</span>
<span style="color: #5B269A">print</span>(<span style="color: #C41A16">"Selected image: \(</span><span style="color: #000000">image</span><span style="color: #C41A16">)"</span>)
}
}
})
}
}
</code></pre></div>
<h2>Filtering and Multi-selection</h2>
<p>By default the selectionLimit is set to 1, and there is no filter.</p>
<p>The change what is selected, and how many, we just need to change the <code>PHPickerConfiguration</code>.<br />
For example: to select up to 10 videos we would need to specify.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">PHPickerConfiguration</span> <span style="color: #000000">*config</span> <span style="color: #000000">=</span> [[<span style="color: #000000">PHPickerConfiguration</span> <span style="color: #000000">alloc</span>] <span style="color: #000000">init</span>];
<span style="color: #000000">config</span>.<span style="color: #000000">selectionLimit</span> <span style="color: #000000">=</span> <span style="color: #1C01CE">10</span>;
<span style="color: #000000">config</span>.<span style="color: #000000">filter</span> <span style="color: #000000">=</span> [<span style="color: #000000">PHPickerFilter</span> <span style="color: #000000">videosFilter</span>];
</code></pre></div>
<p><code>PHPickerConfiguration</code>, <code>PHPickerFilter</code>, and <code>PHPickerResult</code> all bridge across into Swift as Structs, not as classes.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">var</span> <span style="color: #000000">config</span> = <span style="color: #000000">PHPickerConfiguration</span>()
<span style="color: #000000">config</span>.<span style="color: #000000">selectionLimit</span> = <span style="color: #1C01CE">10</span>
<span style="color: #000000">config</span>.<span style="color: #5B269A">filter</span> = <span style="color: #000000">PHPickerFilter</span>.<span style="color: #000000">videos</span>
</code></pre></div>
<p>Setting the selection limit to 0 allows selecting as many items as the system supports.</p>
<h1>Example Projects</h1>
<p><img src="https://ikyle.me/blog/2020/phpickerviewcontroller/example_project.png" alt="The 4 screens of the example Xcode project" /></p>
<p>The example code Xcode Projects demonstrating how to use the new API are available on Github:</p>
<ul>
<li><a href="https://github.com/kylehowells/ikyle.me-code-examples/blob/master/Photo%20Picker%20ObjC/Photo%20Picker%20ObjC/ViewController.m#L99">ObjC Project</a></li>
<li><a href="https://github.com/kylehowells/ikyle.me-code-examples/blob/master/Photo%20Picker%20Swift/Photo%20Picker%20Swift/ViewController.swift#L13">Swift Project</a></li>
</ul>
External Only Shadow on UIView
https://ikyle.me/blog/2020/calayer-external-only-shadow
2020-06-16T03:06:29.634590+00:00
2020-06-16T03:06:29.634590+00:00
How to setup a CALayer shadow to show externally only with CALayer shadowPath and maskLayer
<p>To add a shadow to a <code>UIView</code> is very easy, all you have to do is drop down to the <code>CALayer</code> underneath and setup it's shadow properties.</p>
<p><img src="https://ikyle.me/blog/2020/calayer-external-only-shadow/green-shadow.png" alt="" /></p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Objective C</span>
<span style="color: #000000">view</span>.<span style="color: #000000">layer</span>.<span style="color: #000000">shadowColor</span> <span style="color: #000000">=</span> [<span style="color: #5B269A">UIColor</span> <span style="color: #000000">blackColor</span>].<span style="color: #5B269A">CGColor</span>;
<span style="color: #000000">view</span>.<span style="color: #000000">layer</span>.<span style="color: #000000">shadowOffset</span> <span style="color: #000000">=</span> <span style="color: #000000">CGSizeMake</span>(<span style="color: #1C01CE">0</span>, <span style="color: #1C01CE">1</span>);
<span style="color: #000000">view</span>.<span style="color: #000000">layer</span>.<span style="color: #000000">shadowRadius</span> <span style="color: #000000">=</span> <span style="color: #1C01CE">10</span>;
<span style="color: #000000">view</span>.<span style="color: #000000">layer</span>.<span style="color: #000000">shadowOpacity</span> <span style="color: #000000">=</span> <span style="color: #1C01CE">1.0</span>;
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Swift</span>
<span style="color: #000000">view</span>.<span style="color: #000000">layer</span>.<span style="color: #000000">shadowColor</span> = <span style="color: #5B269A">UIColor</span>.<span style="color: #000000">black</span>.<span style="color: #000000">cgColor</span>
<span style="color: #000000">view</span>.<span style="color: #000000">layer</span>.<span style="color: #000000">shadowOffset</span> = <span style="color: #5B269A">CGSize</span>(<span style="color: #000000">width</span>:<span style="color: #1C01CE">0</span>, <span style="color: #000000">height</span>:<span style="color: #1C01CE">1</span>);
<span style="color: #000000">view</span>.<span style="color: #000000">layer</span>.<span style="color: #000000">shadowRadius</span> = <span style="color: #1C01CE">10</span>;
<span style="color: #000000">view</span>.<span style="color: #000000">layer</span>.<span style="color: #000000">shadowOpacity</span> = <span style="color: #1C01CE">1.0</span>;
</code></pre></div>
<p>However, if your view has a partially, or completely, transparent background color this can look a bit weird.</p>
<p>Original:</p>
<p><img src="https://ikyle.me/blog/2020/calayer-external-only-shadow/original.png" alt="" /></p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// ObjC</span>
<span style="color: #000000">view</span>.<span style="color: #000000">backgroundColor</span> <span style="color: #000000">=</span> [<span style="color: #5B269A">UIColor</span> <span style="color: #000000">colorWithRed</span>:<span style="color: #1C01CE">0</span> <span style="color: #000000">green</span>:<span style="color: #1C01CE">1</span> <span style="color: #000000">blue</span>:<span style="color: #1C01CE">0</span> <span style="color: #000000">alpha</span>:<span style="color: #1C01CE">0.5</span>];
<span style="color: #177500">// Swift</span>
<span style="color: #000000">view</span>.<span style="color: #000000">backgroundColor</span> <span style="color: #000000">=</span> <span style="color: #5B269A">UIColor</span>(<span style="color: #000000">red</span>:<span style="color: #1C01CE">0</span>, <span style="color: #000000">green</span>:<span style="color: #1C01CE">1</span>, <span style="color: #000000">blue</span>:<span style="color: #1C01CE">0</span>, <span style="color: #000000">alpha</span>:<span style="color: #1C01CE">0.5</span>)
</code></pre></div>
<p>With shadow:</p>
<p><img src="https://ikyle.me/blog/2020/calayer-external-only-shadow/green-transparent-shadow.png" alt="" /></p>
<p>To fix this we can use use <code>CALayer</code>'s <code>maskLayer</code> property to create an external only shadow.</p>
<p>First we need to create a mask layer the size and change of our original view.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// ObjC</span>
<span style="color: #5B269A">CAShapeLayer</span> <span style="color: #000000">*maskLayer</span> <span style="color: #000000">=</span> [<span style="color: #5B269A">CAShapeLayer</span> <span style="color: #000000">layer</span>];
<span style="color: #000000">maskLayer</span>.<span style="color: #000000">frame</span> <span style="color: #000000">=</span> <span style="color: #000000">view</span>.<span style="color: #000000">bounds</span>;
<span style="color: #000000">maskLayer</span>.<span style="color: #000000">path</span> <span style="color: #000000">=</span> [<span style="color: #5B269A">UIBezierPath</span> <span style="color: #000000">bezierPathWithRoundedRect</span>:<span style="color: #000000">view</span>.<span style="color: #000000">bounds</span> <span style="color: #000000">cornerRadius</span>:<span style="color: #000000">view</span>.<span style="color: #000000">layer</span>.<span style="color: #000000">cornerRadius</span>].<span style="color: #5B269A">CGPath</span>;
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Swift</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">maskLayer</span> = <span style="color: #5B269A">CAShapeLayer</span>()
<span style="color: #000000">maskLayer</span>.<span style="color: #000000">frame</span> = <span style="color: #000000">view</span>.<span style="color: #000000">bounds</span>
<span style="color: #000000">maskLayer</span>.<span style="color: #000000">path</span> = <span style="color: #5B269A">UIBezierPath</span>(<span style="color: #000000">roundedRect</span>: <span style="color: #000000">view</span>.<span style="color: #000000">bounds</span>, <span style="color: #000000">cornerRadius</span>: <span style="color: #000000">view</span>.<span style="color: #000000">layer</span>.<span style="color: #000000">cornerRadius</span>).<span style="color: #000000">cgPath</span>
</code></pre></div>
<p>Then we need to move it back, up and make it bigger, so it won't clip the shadow itself.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// ObjC</span>
<span style="color: #000000">CGFloat</span> <span style="color: #000000">shadowBorder</span> <span style="color: #000000">=</span> (<span style="color: #000000">view</span>.<span style="color: #000000">layer</span>.<span style="color: #000000">shadowRadius</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">2</span>) <span style="color: #000000">+</span> <span style="color: #1C01CE">5</span>;
<span style="color: #000000">maskLayer</span>.<span style="color: #000000">frame</span> <span style="color: #000000">=</span> <span style="color: #000000">CGRectInset</span>( <span style="color: #000000">maskLayer</span>.<span style="color: #000000">frame</span>, <span style="color: #000000">-shadowBorder</span>, <span style="color: #000000">-shadowBorder</span> ); <span style="color: #177500">// Make bigger</span>
<span style="color: #000000">maskLayer</span>.<span style="color: #000000">frame</span> <span style="color: #000000">=</span> <span style="color: #000000">CGRectOffset</span>( <span style="color: #000000">maskLayer</span>.<span style="color: #000000">frame</span>, <span style="color: #000000">shadowBorder/</span><span style="color: #1C01CE">2.0</span>, <span style="color: #000000">shadowBorder/</span><span style="color: #1C01CE">2.0</span> ); <span style="color: #177500">// Move up and left</span>
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Swift</span>
<span style="color: #177500">// Make the mask area bigger than the view, so the shadow itself does not get clipped by the mask</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">shadowBorder</span>:<span style="color: #000000">CGFloat</span> = (<span style="color: #000000">view</span>.<span style="color: #000000">layer</span>.<span style="color: #000000">shadowRadius</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">2</span>) <span style="color: #000000">+</span> <span style="color: #1C01CE">5</span>;
<span style="color: #000000">maskLayer</span>.<span style="color: #000000">frame</span> = <span style="color: #000000">maskLayer</span>.<span style="color: #000000">frame</span>.<span style="color: #000000">insetBy</span>(<span style="color: #000000">dx</span>: <span style="color: #000000">-shadowBorder</span>, <span style="color: #000000">dy</span>: <span style="color: #000000">-shadowBorder</span>) <span style="color: #177500">// Make bigger</span>
<span style="color: #000000">maskLayer</span>.<span style="color: #000000">frame</span> = <span style="color: #000000">maskLayer</span>.<span style="color: #000000">frame</span>.<span style="color: #000000">offsetBy</span>(<span style="color: #000000">dx</span>: <span style="color: #000000">shadowBorder/</span><span style="color: #1C01CE">2</span>, <span style="color: #000000">dy</span>: <span style="color: #000000">shadowBorder/</span><span style="color: #1C01CE">2</span>) <span style="color: #177500">// Move up and left</span>
</code></pre></div>
<p>And set the fill rule to allow cut outs inside the shape.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">maskLayer</span>.<span style="color: #000000">fillRule</span> <span style="color: #000000">=</span> <span style="color: #000000">kCAFillRuleEvenOdd</span>;
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">maskLayer</span>.<span style="color: #000000">fillRule</span> = <span style="color: #000000">CAShapeLayerFillRule</span>.<span style="color: #000000">evenOdd</span>
</code></pre></div>
<p>Lastly we create a mutable path and use it to rewrite the mask layer's path to one with a cut out a hole in the center where the original view was.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// ObjC</span>
<span style="color: #000000">CGMutablePathRef</span> <span style="color: #000000">pathMasking</span> <span style="color: #000000">=</span> <span style="color: #000000">CGPathCreateMutable</span>();
<span style="color: #177500">// Add the outer view frame</span>
<span style="color: #000000">CGPathAddPath</span>(<span style="color: #000000">pathMasking</span>, <span style="color: #A90D91">NULL</span>, [<span style="color: #5B269A">UIBezierPath</span> <span style="color: #000000">bezierPathWithRect</span>:<span style="color: #000000">maskLayer</span>.<span style="color: #000000">frame</span>].<span style="color: #5B269A">CGPath</span>);
<span style="color: #177500">// Translate into the shape back to the smaller original view's frame start point</span>
<span style="color: #5B269A">CGAffineTransform</span> <span style="color: #000000">catShiftBorder</span> <span style="color: #000000">=</span> <span style="color: #000000">CGAffineTransformMakeTranslation</span>( <span style="color: #000000">shadowBorder/</span><span style="color: #1C01CE">2.0</span>, <span style="color: #000000">shadowBorder/</span><span style="color: #1C01CE">2.0</span>);
<span style="color: #177500">// Now add the original path for the cut out the shape of the original view</span>
<span style="color: #000000">CGPathAddPath</span>(<span style="color: #000000">pathMasking</span>, <span style="color: #A90D91">NULL</span>, <span style="color: #000000">CGPathCreateCopyByTransformingPath</span>(<span style="color: #000000">maskLayer</span>.<span style="color: #000000">path</span>, <span style="color: #000000">&catShiftBorder</span> ) );
<span style="color: #177500">// Set this big rect with a small cutout rect as the mask</span>
<span style="color: #000000">maskLayer</span>.<span style="color: #000000">path</span> <span style="color: #000000">=</span> <span style="color: #000000">pathMasking</span>;
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Swift</span>
<span style="color: #A90D91">let</span> <span style="color: #000000">pathMasking</span> = <span style="color: #000000">CGMutablePath</span>()
<span style="color: #177500">// Add the outer view frame</span>
<span style="color: #000000">pathMasking</span>.<span style="color: #000000">addPath</span>(<span style="color: #5B269A">UIBezierPath</span>(<span style="color: #000000">rect</span>: <span style="color: #000000">maskLayer</span>.<span style="color: #000000">frame</span>).<span style="color: #000000">cgPath</span>)
<span style="color: #177500">// Translate into the shape back to the smaller original view's frame start point</span>
<span style="color: #A90D91">var</span> <span style="color: #000000">catShiftBorder</span> = <span style="color: #5B269A">CGAffineTransform</span>(<span style="color: #000000">translationX</span>: <span style="color: #000000">shadowBorder/</span><span style="color: #1C01CE">2</span>, <span style="color: #000000">y</span>: <span style="color: #000000">shadowBorder/</span><span style="color: #1C01CE">2</span>)
<span style="color: #177500">// Now add the original path for the cut out the shape of the original view</span>
<span style="color: #000000">pathMasking</span>.<span style="color: #000000">addPath</span>(<span style="color: #000000">maskLayer</span>.<span style="color: #000000">path</span>!.<span style="color: #000000">copy</span>(<span style="color: #000000">using</span>: &<span style="color: #000000">catShiftBorder</span>)<span style="color: #000000">!</span>)
<span style="color: #177500">// Set this big rect with a small cutout rect as the mask</span>
<span style="color: #000000">maskLayer</span>.<span style="color: #000000">path</span> = <span style="color: #000000">pathMasking</span>;
</code></pre></div>
<p>Not forgetting to actually set the view's <code>maskLayer</code> to our new layer.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">view</span>.<span style="color: #000000">layer</span>.<span style="color: #000000">mask</span> <span style="color: #000000">=</span> <span style="color: #000000">maskLayer</span>
</code></pre></div>
<p><img src="https://ikyle.me/blog/2020/calayer-external-only-shadow/transparent-shadow.png" alt="" /></p>
<p>This has given us our transparent center, but it will also crop out the background color and any child views placed inside.<br />
As a result, this technique requires a full size 'shadow view' added as the bottom most child of the view you want to add a shadow to, rather than applying this directly to the view itself.</p>
<p><img src="https://ikyle.me/blog/2020/calayer-external-only-shadow/result.png" alt="" /></p>
<h1>Example Xcode Projects</h1>
<p>There are Swift and Objective C example Xcode projects showing this technique available on Github:</p>
<ul>
<li><a href="https://github.com/kylehowells/ikyle.me-code-examples/blob/master/External%20Shadow%20ObjC/External%20Shadow/ViewController.m">Objective C</a></li>
<li><a href="https://github.com/kylehowells/ikyle.me-code-examples/blob/master/External%20Shadow%20Swift/External%20Shadow%20Swift/ViewController.swift">Swift</a></li>
</ul>
CAGradientLayer Explained
https://ikyle.me/blog/2020/cagradientlayer-explained
2020-06-15T22:19:33.819892+00:00
2020-06-15T00:22:36.789987+00:00
How to natively draw gradients on iOS and macOS with CAGradientLayer
<p>iOS and macOS have built in support for drawing gradients in QuartzCore with the CAGradientLayer class, no custom CoreGraphics drawing in <code>-drawRect:</code> with <a href="https://stackoverflow.com/questions/9883320/how-to-draw-cggradient-with-specific-color"><code>CGContextDrawLinearGradient</code></a> necessary.</p>
<p>Another advantage of CAGradientLayer over using CoreGraphics, besides the convenience, is that CAGradientLayer is rendered directly by the GPU itself. Compared to drawing a gradient with CoreGraphics, which will draw into a buffer on the CPU and then send that to the GPU.</p>
<h1>Linear (Axial) Gradients</h1>
<p>To create a linear gradient from one color to another (or several) the layer's <code>type</code> must be set to <code>kCAGradientLayerAxial</code> (<code>.axial</code>). The colors must be CGColor's, not UIColors. If you forgot this the color object is just ignored completely.</p>
<p><img src="https://ikyle.me/blog/2020/cagradientlayer-explained/linear.png" alt="" /></p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Objective C</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">colors</span> <span style="color: #000000">=</span>
<span style="color: #1C01CE">@[</span>
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">colorWithRed</span>: <span style="color: #1C01CE">48.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">green</span>: <span style="color: #1C01CE">35.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">blue</span>: <span style="color: #1C01CE">174.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>].<span style="color: #5B269A">CGColor</span>,
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">colorWithRed</span>: <span style="color: #1C01CE">200.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">green</span>: <span style="color: #1C01CE">109.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">blue</span>: <span style="color: #1C01CE">215.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>].<span style="color: #5B269A">CGColor</span>
<span style="color: #1C01CE">]</span>;
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Swift</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">colors</span> =
[
<span style="color: #5B269A">UIColor</span>(<span style="color: #000000">red</span>: <span style="color: #1C01CE">48.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">green</span>: <span style="color: #1C01CE">35.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">blue</span>: <span style="color: #1C01CE">174.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>).<span style="color: #000000">cgColor</span>,
<span style="color: #5B269A">UIColor</span>(<span style="color: #000000">red</span>: <span style="color: #1C01CE">200.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">green</span>: <span style="color: #1C01CE">109.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">blue</span>: <span style="color: #1C01CE">215.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>).<span style="color: #000000">cgColor</span>
]
</code></pre></div>
<h2>Start Point and End Point</h2>
<p>The start and end point of the gradient can be changed to get different effects.<br />
For gradients the X and Y axis go from 0 to 1, across their length. So a start point of <code>(x:0, y:0)</code> is the top left corner.</p>
<p>By default the start point is <code>(0.5, 0.0)</code>, half way across the horizontal and at the top vertically, and the end point is the bottom middle of the view <code>(0.5, 1.0)</code>.</p>
<p><img src="https://ikyle.me/blog/2020/cagradientlayer-explained/corners.png" alt="" /></p>
<p><img src="https://ikyle.me/blog/2020/cagradientlayer-explained/linear-horizontal.png" alt="" /></p>
<p>The code to create this example horizontal gradient is:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Objective C</span>
<span style="color: #177500">// Set type (Axial is already the default value)</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">type</span> <span style="color: #000000">=</span> <span style="color: #000000">kCAGradientLayerAxial</span>;
<span style="color: #177500">// Set the colors (these need to be CGColor's, not UIColor's)</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">colors</span> <span style="color: #000000">=</span>
<span style="color: #1C01CE">@[</span>
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">colorWithRed</span>: <span style="color: #1C01CE">48.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">green</span>: <span style="color: #1C01CE">35.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">blue</span>: <span style="color: #1C01CE">174.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>].<span style="color: #5B269A">CGColor</span>,
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">colorWithRed</span>: <span style="color: #1C01CE">200.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">green</span>: <span style="color: #1C01CE">109.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">blue</span>: <span style="color: #1C01CE">215.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>].<span style="color: #5B269A">CGColor</span>
<span style="color: #1C01CE">]</span>;
<span style="color: #177500">// Set the start and end points</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">startPoint</span> <span style="color: #000000">=</span> <span style="color: #000000">CGPointMake</span>(<span style="color: #1C01CE">0</span>, <span style="color: #1C01CE">0</span>);
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">endPoint</span> <span style="color: #000000">=</span> <span style="color: #000000">CGPointMake</span>(<span style="color: #1C01CE">1</span>, <span style="color: #1C01CE">0</span>);
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Swift</span>
<span style="color: #177500">// Set type (Axial is already the default value)</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">type</span> = <span style="color: #000000">CAGradientLayerType</span>.<span style="color: #000000">axial</span>
<span style="color: #177500">// Set the colors (these need to be CGColor's, not UIColor's)</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">colors</span> =
[
<span style="color: #5B269A">UIColor</span>(<span style="color: #000000">red</span>: <span style="color: #1C01CE">48.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">green</span>: <span style="color: #1C01CE">35.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">blue</span>: <span style="color: #1C01CE">174.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>).<span style="color: #000000">cgColor</span>,
<span style="color: #5B269A">UIColor</span>(<span style="color: #000000">red</span>: <span style="color: #1C01CE">200.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">green</span>: <span style="color: #1C01CE">109.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">blue</span>: <span style="color: #1C01CE">215.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>).<span style="color: #000000">cgColor</span>
]
<span style="color: #177500">// Set the start and end points</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">startPoint</span> = <span style="color: #5B269A">CGPoint</span>(<span style="color: #000000">x</span>: <span style="color: #1C01CE">0</span>, <span style="color: #000000">y</span>: <span style="color: #1C01CE">0</span>)
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">endPoint</span> = <span style="color: #5B269A">CGPoint</span>(<span style="color: #000000">x</span>: <span style="color: #1C01CE">1</span>, <span style="color: #000000">y</span>: <span style="color: #1C01CE">0</span>)
</code></pre></div>
<h2>Multiple Colors and Positions</h2>
<p>Most of my uses for gradients only involve 2 colors, but the colors property is an array of colors are you can add as many as you want.</p>
<p><img src="https://ikyle.me/blog/2020/cagradientlayer-explained/rainbow.png" alt="" /></p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">gradientLayer</span>.<span style="color: #000000">colors</span> <span style="color: #000000">=</span>
<span style="color: #1C01CE">@[</span>
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">blueColor</span>].<span style="color: #5B269A">CGColor</span>,
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">orangeColor</span>].<span style="color: #5B269A">CGColor</span>,
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">greenColor</span>].<span style="color: #5B269A">CGColor</span>,
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">redColor</span>].<span style="color: #5B269A">CGColor</span>,
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">purpleColor</span>].<span style="color: #5B269A">CGColor</span>
<span style="color: #1C01CE">]</span>;
</code></pre></div>
<p>The <code>locations</code> property can be used to control how the colors are distributed across the layer.</p>
<p><img src="https://ikyle.me/blog/2020/cagradientlayer-explained/rainbox-locations.png" alt="" /></p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// ObjC</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">locations</span> <span style="color: #000000">=</span> <span style="color: #1C01CE">@[</span>
<span style="color: #1C01CE">@0</span>, <span style="color: #177500">// blueColor</span>
<span style="color: #1C01CE">@0.1</span>, <span style="color: #177500">// orangeColor</span>
<span style="color: #1C01CE">@0.6</span>, <span style="color: #177500">// greenColor</span>
<span style="color: #1C01CE">@0.7</span>, <span style="color: #177500">// redColor</span>
<span style="color: #1C01CE">@1</span> <span style="color: #177500">// purpleColor</span>
<span style="color: #1C01CE">]</span>;
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Swift</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">locations</span> = [
<span style="color: #1C01CE">0</span>, <span style="color: #177500">// blueColor</span>
<span style="color: #1C01CE">0.1</span>, <span style="color: #177500">// orangeColor</span>
<span style="color: #1C01CE">0.6</span>, <span style="color: #177500">// greenColor</span>
<span style="color: #1C01CE">0.7</span>, <span style="color: #177500">// redColor</span>
<span style="color: #1C01CE">1</span> <span style="color: #177500">// purpleColor</span>
]
</code></pre></div>
<h1>Radial Gradients</h1>
<p>To create a radial gradient out from one point the layer's <code>type</code> must be set to <code>kCAGradientLayerRadial</code> (<code>.radial</code>).</p>
<p>The start and end point are important, as the <code>endPoint</code>'s <code>x</code> and <code>y</code> define the end of the second color.</p>
<p>With a <code>startPoint</code> in the center <code>(0.5, 0.5)</code>, an <code>endPoint</code> <code>x</code> of 0 or 1 will draw from the center to the outer edge of the view horizontally.<br />
An <code>endPoint</code> <code>Y</code> of 1 or 0 will draw the gradient to the height of the view.</p>
<p><img src="https://ikyle.me/blog/2020/cagradientlayer-explained/radial-center.png" alt="" /></p>
<p>The code to create this example gradient is:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Objective C</span>
<span style="color: #177500">// Set the type</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">type</span> <span style="color: #000000">=</span> <span style="color: #000000">kCAGradientLayerRadial</span>;
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">colors</span> <span style="color: #000000">=</span>
<span style="color: #1C01CE">@[</span>
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">colorWithRed</span>: <span style="color: #1C01CE">0.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">green</span>: <span style="color: #1C01CE">101.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">blue</span>: <span style="color: #1C01CE">255.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>].<span style="color: #5B269A">CGColor</span>,
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">colorWithRed</span>: <span style="color: #1C01CE">0.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">green</span>: <span style="color: #1C01CE">40.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">blue</span>: <span style="color: #1C01CE">101.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>].<span style="color: #5B269A">CGColor</span>
<span style="color: #1C01CE">]</span>;
<span style="color: #177500">// Start in the center</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">startPoint</span> <span style="color: #000000">=</span> <span style="color: #000000">CGPointMake</span>(<span style="color: #1C01CE">0.5</span>, <span style="color: #1C01CE">0.5</span>);
<span style="color: #177500">// End at the outer edge of the view</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">endPoint</span> <span style="color: #000000">=</span> <span style="color: #000000">CGPointMake</span>(<span style="color: #1C01CE">0</span>, <span style="color: #1C01CE">0.75</span>);
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Swift</span>
<span style="color: #177500">// Set type to radial</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">type</span> = <span style="color: #000000">CAGradientLayerType</span>.<span style="color: #000000">radial</span>
<span style="color: #177500">// Set the colors</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">colors</span> =
[
<span style="color: #5B269A">UIColor</span>(<span style="color: #000000">red</span>: <span style="color: #1C01CE">0.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">green</span>: <span style="color: #1C01CE">101.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">blue</span>: <span style="color: #1C01CE">255.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>).<span style="color: #000000">cgColor</span>,
<span style="color: #5B269A">UIColor</span>(<span style="color: #000000">red</span>: <span style="color: #1C01CE">0.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">green</span>: <span style="color: #1C01CE">40.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">blue</span>: <span style="color: #1C01CE">101.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>).<span style="color: #000000">cgColor</span>
]
<span style="color: #177500">// Start point of first color in the middle of the view</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">startPoint</span> = <span style="color: #5B269A">CGPoint</span>(<span style="color: #000000">x</span>: <span style="color: #1C01CE">0.5</span>, <span style="color: #000000">y</span>: <span style="color: #1C01CE">0.5</span>)
<span style="color: #177500">// End points to the edges of the view</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">endPoint</span> = <span style="color: #5B269A">CGPoint</span>(<span style="color: #000000">x</span>: <span style="color: #1C01CE">0</span>, <span style="color: #000000">y</span>: <span style="color: #1C01CE">0.75</span>)
</code></pre></div>
<h1>Angular (Conic) Gradients</h1>
<p>Whereas linear and radial gradients have been part of the iOS SDK forever (iOS 3.0 & 3.2 respectively & macOS 10.6) angular, or conic as CoreAnimation calls them, gradients were only added in iOS 12.0 (macOS 10.14).</p>
<p>The <code>startPoint</code> is the point at which the gradient is drawn around.<br />
The <code>endPoint</code> is the point the start/end line draws aiming at.</p>
<p><img src="https://ikyle.me/blog/2020/cagradientlayer-explained/conic-labels.png" alt="" /></p>
<p>The code to create the example angular gradient is:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Objective C</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">type</span> <span style="color: #000000">=</span> <span style="color: #000000">kCAGradientLayerConic</span>;
<span style="color: #177500">// Set the colors</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">colors</span> <span style="color: #000000">=</span>
<span style="color: #1C01CE">@[</span>
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">blueColor</span>].<span style="color: #5B269A">CGColor</span>,
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">colorWithRed</span>: <span style="color: #1C01CE">50.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">green</span>: <span style="color: #1C01CE">251.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">blue</span>: <span style="color: #1C01CE">255.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span> <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>].<span style="color: #5B269A">CGColor</span>,
(<span style="color: #A90D91">id</span>)[<span style="color: #5B269A">UIColor</span> <span style="color: #000000">blackColor</span>].<span style="color: #5B269A">CGColor</span>
<span style="color: #1C01CE">]</span>;
<span style="color: #177500">// Start point of first color in the middle of the view</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">startPoint</span> <span style="color: #000000">=</span> <span style="color: #000000">CGPointMake</span>(<span style="color: #1C01CE">0.5</span>, <span style="color: #1C01CE">0.5</span>);
<span style="color: #177500">// End points to the edges of the view</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">endPoint</span> <span style="color: #000000">=</span> <span style="color: #000000">CGPointMake</span>(<span style="color: #1C01CE">0.5</span>, <span style="color: #1C01CE">0</span>);
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Swift</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">type</span> = <span style="color: #000000">CAGradientLayerType</span>.<span style="color: #000000">conic</span>
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">colors</span> =
[
<span style="color: #5B269A">UIColor</span>.<span style="color: #000000">blue</span>,
<span style="color: #5B269A">UIColor</span>(<span style="color: #000000">red</span>: <span style="color: #1C01CE">50.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">green</span>: <span style="color: #1C01CE">251.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">blue</span>: <span style="color: #1C01CE">255.0</span><span style="color: #000000">/</span><span style="color: #1C01CE">255.0</span>, <span style="color: #000000">alpha</span>: <span style="color: #1C01CE">1.0</span>).<span style="color: #000000">cgColor</span>,
<span style="color: #5B269A">UIColor</span>.<span style="color: #000000">black</span>
]
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">startPoint</span> = <span style="color: #5B269A">CGPoint</span>(<span style="color: #000000">x</span>: <span style="color: #1C01CE">0.5</span>, <span style="color: #000000">y</span>: <span style="color: #1C01CE">0.5</span>)
<span style="color: #000000">gradientLayer</span>.<span style="color: #000000">endPoint</span> = <span style="color: #5B269A">CGPoint</span>(<span style="color: #000000">x</span>: <span style="color: #1C01CE">0.5</span>, <span style="color: #000000">y</span>: <span style="color: #1C01CE">0</span>)
</code></pre></div>
<h1>UIView Wrapper</h1>
<p>As layers are a little annoying to work with in UIKit, with implicit animations and separate layout, I almost always use it via wrapping it in a custom UIView subclass.<br />
You can actually change the underlying type of a <code>UIView</code> by overriding the <code>+(Class)layerClass</code> class method and returning any CALayer subclass.</p>
<p>Objective C Subclass. Redefining the layer property means <code>view.layer</code> will happily return the correct layer class, and <code>@dynamic layer;</code> tells it not to auto-synthesize anything for that deceleration.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// KHGradientView.h</span>
<span style="color: #A90D91">@import</span> <span style="color: #000000">UIKit</span>;
<span style="color: #A90D91">@import</span> <span style="color: #000000">QuartzCore</span>;
<span style="color: #A90D91">@interface</span> <span style="color: #3F6E75">KHGradientView</span> : <span style="color: #5B269A">UIView</span>
<span style="color: #A90D91">@property</span>(<span style="color: #A90D91">nonatomic</span>, <span style="color: #A90D91">readonly</span>, <span style="color: #A90D91">strong</span>) <span style="color: #5B269A">CAGradientLayer</span> <span style="color: #000000">*layer</span>;
<span style="color: #A90D91">@end</span>
<span style="color: #177500">// KHGradientView.m</span>
<span style="color: #633820">#import "KHGradientView.h"</span>
<span style="color: #A90D91">@implementation</span> <span style="color: #3F6E75">KHGradientView</span>
<span style="color: #A90D91">@dynamic</span> <span style="color: #000000">layer</span>;
+(<span style="color: #A90D91">Class</span>)<span style="color: #000000">layerClass</span>{
<span style="color: #A90D91">return</span> [<span style="color: #5B269A">CAGradientLayer</span> <span style="color: #A90D91">class</span>];
}
<span style="color: #A90D91">@end</span>
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// Swift Subclass</span>
<span style="color: #A90D91">import</span> <span style="color: #3F6E75">UIKit</span>
<span style="color: #A90D91">class</span> <span style="color: #3F6E75">KHGradientView</span>: <span style="color: #5B269A">UIView</span> {
<span style="color: #A90D91">override</span> <span style="color: #000000">open</span> <span style="color: #A90D91">class</span> <span style="color: #3F6E75">var</span> <span style="color: #000000">layerClass</span>: <span style="color: #A90D91">AnyClass</span> {
<span style="color: #A90D91">return</span> <span style="color: #5B269A">CAGradientLayer</span>.<span style="color: #000000">classForCoder</span>()
}
<span style="color: #A90D91">var</span> <span style="color: #000000">gradientLayer</span> : <span style="color: #5B269A">CAGradientLayer</span> {
<span style="color: #A90D91">return</span> <span style="color: #A90D91">self</span>.<span style="color: #000000">layer</span> <span style="color: #A90D91">as</span>! <span style="color: #5B269A">CAGradientLayer</span>
}
}
</code></pre></div>
<p>It's incredibly minimal, but makes things much easier in the rest of the codebase.</p>
<h1>Example Project</h1>
<p><img src="https://ikyle.me/blog/2020/cagradientlayer-explained/screenshot.png" alt="Example linear, radial, and angular gradients" /></p>
<p>The example code Xcode Project to produce the opening example image is available on Github:</p>
<ul>
<li><a href="https://github.com/kylehowells/ikyle.me-code-examples/blob/master/CAGradientLayer%20Example%20ObjC/CAGradientLayer%20Example%20ObjC/ViewController.m">ObjC Project</a></li>
<li><a href="https://github.com/kylehowells/ikyle.me-code-examples/blob/master/CAGradientLayer%20Example%20Swift/CAGradientLayer%20Example%20Swift/ViewController.swift">Swift Project</a></li>
</ul>
Mac Catalyst Title Bar Window Tabs
https://ikyle.me/blog/2020/mac-catalyst-nstitlebar-tabs
2020-06-01T00:57:38.826645+00:00
2020-06-01T00:57:38.826645+00:00
How to setup NSTitleBar style tabs for a UIKit app on mac with Catalyst
<p>To start with let's take a simple tab bar based app with a label in the <code>UIViewController</code>'s' telling us which view we are looking at.<br />
If we just take this app and compile and run it for mac Catalyst this is the result we get.</p>
<p><img src="https://ikyle.me/blog/2020/mac-catalyst-nstitlebar-tabs/AppStartingPoint.png" alt="Screenshot of the UITabBarController app on iPhone 8 and macOS Catalina" /></p>
<p>This works... but feels a lot like running the app in the iOS simulator.<br />
You probably want to change the style, to either the side tabs, like the AppStore app, or titlebar tabs, like the Calendar app.</p>
<p><img src="https://ikyle.me/blog/2020/mac-catalyst-nstitlebar-tabs/Apple_Apps_Screenshots.png" alt="Screenshot of the AppStore and Calendar apps on Catalina" /></p>
<p>Most of the time I want the tabs in the titlebar. (if you want the sidebar style, that's <code>UISplitViewController</code> + <code>.primaryBackgroundStyle = .sidebar</code>)</p>
<h2>Putting Tabs in the Title Bar</h2>
<p>To do that we need to customise the window's tilebar. In iOS, the application window (as in multi-window support, not <code>UIWindow</code> or <code>NSWindow</code>) is now controlled from the <code>UISceneDelegate</code>.</p>
<p>In the SceneDelegate we setup an <code>NSToolbar</code>, set the <code>SceneDelegate</code> as the <code>NSToolbar</code> delegate and then hide both the title bar's title itself, after assigning the toolbar, & the tab bar.<br />
Inside the SceneDelegate class add:</p>
<p>Objective-C (ObjC)<br />
SceneDelegate.m</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// SceneDelegate.m</span>
<span style="color: #633820">#if TARGET_OS_MACCATALYST</span>
<span style="color: #633820">#import <AppKit/AppKit.h></span>
<span style="color: #A90D91">@interface</span> <span style="color: #3F6E75">SceneDelegate</span> () <span style="color: #000000"><NSToolbarDelegate></span>
<span style="color: #633820">#else</span>
<span style="color: #A90D91">@interface</span> <span style="color: #3F6E75">SceneDelegate</span> ()
<span style="color: #633820">#endif</span>
<span style="color: #177500">// -[SceneDelegate scene:willConnectToSession:options:] {</span>
<span style="color: #633820">#if TARGET_OS_MACCATALYST</span>
<span style="color: #000000">NSToolbar</span> <span style="color: #000000">*toolbar</span> <span style="color: #000000">=</span> [[<span style="color: #000000">NSToolbar</span> <span style="color: #000000">alloc</span>] <span style="color: #000000">initWithIdentifier</span>:<span style="color: #C41A16">@"mainToolbar"</span>];
<span style="color: #000000">toolbar</span>.<span style="color: #000000">centeredItemIdentifier</span> <span style="color: #000000">=</span> <span style="color: #C41A16">@"mainTabsToolbarItem"</span>;
<span style="color: #000000">toolbar</span>.<span style="color: #000000">delegate</span> <span style="color: #000000">=</span> <span style="color: #A90D91">self</span>;
[(<span style="color: #000000">UIWindowScene*</span>)<span style="color: #000000">scene</span> <span style="color: #000000">titlebar</span>].<span style="color: #000000">toolbar</span> <span style="color: #000000">=</span> <span style="color: #000000">toolbar</span>;
[(<span style="color: #000000">UIWindowScene*</span>)<span style="color: #000000">scene</span> <span style="color: #000000">titlebar</span>].<span style="color: #000000">titleVisibility</span> <span style="color: #000000">=</span> <span style="color: #000000">UITitlebarTitleVisibilityHidden</span>;
<span style="color: #000000">_tabbarController</span>.<span style="color: #000000">tabBar</span>.<span style="color: #000000">hidden</span> <span style="color: #000000">=</span> <span style="color: #A90D91">YES</span>;
<span style="color: #633820">#endif</span>
</code></pre></div>
<p>Swift<br />
SceneDelegate.swift</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// SceneDelegate.swift</span>
<span style="color: #177500">// SceneDelegate.scene(_:willConnectTo:options:) {</span>
<span style="color: #633820">#if</span> <span style="color: #633820">targetEnvironment</span>(<span style="color: #633820">macCatalyst</span>)
<span style="color: #A90D91">let</span> <span style="color: #000000">toolbar</span> = <span style="color: #000000">NSToolbar</span>(<span style="color: #000000">identifier</span>: <span style="color: #C41A16">"mainToolbar"</span>)
<span style="color: #000000">toolbar</span>.<span style="color: #000000">centeredItemIdentifier</span> = <span style="color: #000000">NSToolbarItem</span>.<span style="color: #000000">Identifier</span>(<span style="color: #000000">rawValue</span>: <span style="color: #C41A16">"mainTabsToolbarItem"</span>)
<span style="color: #000000">toolbar</span>.<span style="color: #000000">delegate</span> = <span style="color: #A90D91">self</span>
<span style="color: #000000">windowScene</span>.<span style="color: #000000">titlebar</span>?.<span style="color: #000000">titleVisibility</span> = .<span style="color: #000000">hidden</span>
<span style="color: #000000">windowScene</span>.<span style="color: #000000">titlebar</span>?.<span style="color: #000000">toolbar</span> = <span style="color: #000000">toolbar</span>
<span style="color: #000000">tabBarController</span>.<span style="color: #000000">tabBar</span>.<span style="color: #000000">isHidden</span> = <span style="color: #A90D91">true</span>
<span style="color: #633820">#endif</span>
</code></pre></div>
<p>Most of the <code>NSToolbar</code> stuff is actually configured via the <code>NSToolbarDelegate</code> methods.</p>
<p>Here we are: specifying which items can be added to the toolbar <code>toolbarDefaultItemIdentifiers:</code> & <code>toolbarAllowedItemIdentifiers:</code>; setting up a new <code>NSToolbarItemGroup</code> item, specifying the segment control in the center of the title bar; and then .</p>
<p>ObjC</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// SceneDelegate.m</span>
<span style="color: #633820">#pragma mark - NSToolbarDelegate</span>
<span style="color: #633820">#if TARGET_OS_MACCATALYST</span>
<span style="color: #177500">// NSToolbarItemIdentifier is just an NSString*</span>
- (<span style="color: #000000">nullable</span> <span style="color: #000000">NSToolbarItem</span> <span style="color: #000000">*</span>)<span style="color: #000000">toolbar:</span>(<span style="color: #000000">NSToolbar</span> <span style="color: #000000">*</span>)<span style="color: #000000">toolbar</span> <span style="color: #000000">itemForItemIdentifier:</span>(<span style="color: #000000">NSToolbarItemIdentifier</span>)<span style="color: #000000">itemIdentifier</span> <span style="color: #000000">willBeInsertedIntoToolbar:</span>(<span style="color: #A90D91">BOOL</span>)<span style="color: #000000">flag</span>
{
<span style="color: #A90D91">if</span> ([<span style="color: #000000">itemIdentifier</span> <span style="color: #000000">isEqualToString</span>:<span style="color: #C41A16">@"mainTabsToolbarItem"</span>]) {
<span style="color: #000000">NSToolbarItemGroup</span> <span style="color: #000000">*group</span> <span style="color: #000000">=</span> [<span style="color: #000000">NSToolbarItemGroup</span> <span style="color: #000000">groupWithItemIdentifier</span>:<span style="color: #000000">itemIdentifier</span>
<span style="color: #000000">titles</span>:<span style="color: #1C01CE">@[</span><span style="color: #C41A16">@"First"</span>, <span style="color: #C41A16">@"Second"</span><span style="color: #1C01CE">]</span>
<span style="color: #000000">selectionMode</span>:<span style="color: #000000">NSToolbarItemGroupSelectionModeSelectOne</span>
<span style="color: #000000">labels</span>:<span style="color: #1C01CE">@[</span><span style="color: #C41A16">@"First view"</span>, <span style="color: #C41A16">@"Second view"</span><span style="color: #1C01CE">]</span>
<span style="color: #000000">target</span>:<span style="color: #A90D91">self</span>
<span style="color: #000000">action</span>:<span style="color: #A90D91">@selector</span>(<span style="color: #000000">toolbarGroupSelectionChanged</span>:)];
[<span style="color: #000000">group</span> <span style="color: #000000">setSelectedIndex</span>:<span style="color: #1C01CE">0</span>];
<span style="color: #A90D91">return</span> <span style="color: #000000">group</span>;
}
<span style="color: #A90D91">return</span> <span style="color: #A90D91">nil</span>;
}
<span style="color: #177500">/* Returns the ordered list of items to be shown in the toolbar by default. If during initialization, no overriding values are found in the user defaults, or if the user chooses to revert to the default items this set will be used. */</span>
- (<span style="color: #5B269A">NSArray</span><span style="color: #000000"><NSToolbarItemIdentifier></span> <span style="color: #000000">*</span>)<span style="color: #000000">toolbarDefaultItemIdentifiers:</span>(<span style="color: #000000">NSToolbar</span> <span style="color: #000000">*</span>)<span style="color: #000000">toolbar</span>{
<span style="color: #A90D91">return</span> <span style="color: #1C01CE">@[</span><span style="color: #C41A16">@"mainTabsToolbarItem"</span><span style="color: #1C01CE">]</span>;
}
<span style="color: #177500">/* Returns the list of all allowed items by identifier. By default, the toolbar does not assume any items are allowed, even the separator. So, every allowed item must be explicitly listed. The set of allowed items is used to construct the customization palette. The order of items does not necessarily guarantee the order of appearance in the palette. At minimum, you should return the default item list.*/</span>
- (<span style="color: #5B269A">NSArray</span><span style="color: #000000"><NSToolbarItemIdentifier></span> <span style="color: #000000">*</span>)<span style="color: #000000">toolbarAllowedItemIdentifiers:</span>(<span style="color: #000000">NSToolbar</span> <span style="color: #000000">*</span>)<span style="color: #000000">toolbar</span>{
<span style="color: #A90D91">return</span> [<span style="color: #A90D91">self</span> <span style="color: #000000">toolbarDefaultItemIdentifiers</span>:<span style="color: #000000">toolbar</span>];
}
-(<span style="color: #A90D91">void</span>)<span style="color: #000000">toolbarGroupSelectionChanged:</span>(<span style="color: #000000">NSToolbarItemGroup*</span>)<span style="color: #000000">sender</span>{
<span style="color: #A90D91">self</span>.<span style="color: #000000">tabbarController</span>.<span style="color: #000000">selectedIndex</span> <span style="color: #000000">=</span> <span style="color: #000000">sender</span>.<span style="color: #000000">selectedIndex</span>;
}
<span style="color: #633820">#endif</span>
</code></pre></div>
<p>Swift</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500">// SceneDelegate.swift</span>
<span style="color: #177500">// MARK: - NSToolbarDelegate</span>
<span style="color: #A90D91">func</span> <span style="color: #000000">toolbar</span>(<span style="color: #A90D91">_</span> <span style="color: #000000">toolbar</span>: <span style="color: #000000">NSToolbar</span>, <span style="color: #000000">itemForItemIdentifier</span> <span style="color: #000000">itemIdentifier</span>: <span style="color: #000000">NSToolbarItem</span>.<span style="color: #000000">Identifier</span>, <span style="color: #000000">willBeInsertedIntoToolbar</span> <span style="color: #000000">flag</span>: <span style="color: #A90D91">Bool</span>) -> <span style="color: #000000">NSToolbarItem</span>?
{
<span style="color: #A90D91">if</span> (<span style="color: #000000">itemIdentifier</span>.<span style="color: #000000">rawValue</span> == <span style="color: #C41A16">"mainTabsToolbarItem"</span>)
{
<span style="color: #A90D91">let</span> <span style="color: #000000">group</span> = <span style="color: #000000">NSToolbarItemGroup</span>.<span style="color: #A90D91">init</span>(<span style="color: #000000">itemIdentifier</span>: <span style="color: #000000">itemIdentifier</span>,
<span style="color: #000000">titles</span>: [<span style="color: #C41A16">"First"</span>, <span style="color: #C41A16">"Second"</span>],
<span style="color: #000000">selectionMode</span>: .<span style="color: #000000">selectOne</span>,
<span style="color: #000000">labels</span>: [<span style="color: #C41A16">"First view"</span>, <span style="color: #C41A16">"Second view"</span>],
<span style="color: #000000">target</span>: <span style="color: #A90D91">self</span>, <span style="color: #000000">action</span>: <span style="color: #A90D91">#selector</span>(<span style="color: #000000">toolbarGroupSelectionChanged</span>))
<span style="color: #000000">group</span>.<span style="color: #000000">setSelected</span>(<span style="color: #A90D91">true</span>, <span style="color: #000000">at</span>: <span style="color: #1C01CE">0</span>)
<span style="color: #A90D91">return</span> <span style="color: #000000">group</span>
}
<span style="color: #A90D91">return</span> <span style="color: #A90D91">nil</span>
}
<span style="color: #A90D91">func</span> <span style="color: #000000">toolbarDefaultItemIdentifiers</span>(<span style="color: #A90D91">_</span> <span style="color: #000000">toolbar</span>: <span style="color: #000000">NSToolbar</span>) -> [<span style="color: #000000">NSToolbarItem</span>.<span style="color: #000000">Identifier</span>] {
<span style="color: #A90D91">return</span> [<span style="color: #000000">NSToolbarItem</span>.<span style="color: #000000">Identifier</span>(<span style="color: #000000">rawValue</span>: <span style="color: #C41A16">"mainTabsToolbarItem"</span>)]
}
<span style="color: #A90D91">func</span> <span style="color: #000000">toolbarAllowedItemIdentifiers</span>(<span style="color: #A90D91">_</span> <span style="color: #000000">toolbar</span>: <span style="color: #000000">NSToolbar</span>) -> [<span style="color: #000000">NSToolbarItem</span>.<span style="color: #000000">Identifier</span>] {
<span style="color: #A90D91">return</span> <span style="color: #A90D91">self</span>.<span style="color: #000000">toolbarDefaultItemIdentifiers</span>(<span style="color: #000000">toolbar</span>)
}
<span style="color: #A90D91">@objc</span> <span style="color: #A90D91">func</span> <span style="color: #000000">toolbarGroupSelectionChanged</span>(<span style="color: #A90D91">_</span> <span style="color: #000000">sender</span>: <span style="color: #000000">NSToolbarItemGroup</span>) {
<span style="color: #000000">tabBarController</span>.<span style="color: #000000">selectedIndex</span> = <span style="color: #000000">sender</span>.<span style="color: #000000">selectedIndex</span>
}
</code></pre></div>
<p>If you get the error message:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">ERROR:</span> <span style="color: #000000">Declaration</span> <span style="color: #000000">of</span> <span style="color: #C41A16">'NSToolbarItemGroup'</span> <span style="color: #000000">must</span> <span style="color: #000000">be</span> <span style="color: #000000">imported</span> <span style="color: #000000">from</span> <span style="color: #000000">module</span> <span style="color: #C41A16">'AppKit.NSToolbarItemGroup'</span> <span style="color: #000000">before</span> <span style="color: #000000">it</span> <span style="color: #A90D91">is</span> <span style="color: #000000">required</span>
</code></pre></div>
<p>This means you haven't imported the Appkit headers. The fix is to <code>@include AppKit;</code> or <code>#import <AppKit/AppKit.h></code>.</p>
<p>Before and after:</p>
<p><img src="https://ikyle.me/blog/2020/mac-catalyst-nstitlebar-tabs/Before_After.png" alt="Example application using UITabBarController and NSToolbar" /></p>
<p>Looking at the result it might seem that the <code>labels</code> part has been ignored. However, if you turn on customisation of the toolbar, or remove the hiding of the title in the titlebar you can see where it is used.</p>
<p><img src="https://ikyle.me/blog/2020/mac-catalyst-nstitlebar-tabs/Toolbar_customisation.png" alt="Example application with visible title, visible labels and customisation panel open" /></p>
<h2>Example Projects</h2>
<p>Made with Xcode 11.4.1 targeting iOS 13.0</p>
<ul>
<li><a href="https://github.com/kylehowells/ikyle.me-code-examples/tree/master/macCatalyst-titlebar-tabbar-objc">Minimal Objective C Example Project</a></li>
<li><a href="https://github.com/kylehowells/ikyle.me-code-examples/tree/master/macCatalyst-titlebar-tabbar-swift">Minimal Swift 4 Example Project</a></li>
</ul>
Spherium: 360º Photos Explained
https://ikyle.me/blog/2020/spherium-360-photos-explained
2020-04-27T23:29:15.119018+00:00
2020-04-27T23:29:15.119018+00:00
How Spherium detects 360º photos on iOS
<p>Spherium searches your iOS library for 360º degree equirectangular images.</p>
<h1>What is a 360 Photo</h1>
<p>Example</p>
<p><img src="https://ikyle.me/portfolio/spherium/sample_photos/city-small.jpg" alt="Ariel 360 photo of a city" /></p>
<p>Normal photos capture what is in front of the camera. 360º photos capture what is all around the camera in every direction.</p>
<h1>What is Equirectangular Projection</h1>
<p>The <a href="https://en.wikipedia.org/wiki/Equirectangular_projection">wikipedia article</a> on it has more information, but effectively a equirectangular projection is a way of stretching the pixels from a sphere into a flat 2:1 image.</p>
<p>Spherium looks for images which are exactly 2:1 (w:h) to indicate they might be a 360º photo.</p>
<h1>How Do I Take a 360 Photo</h1>
<p>You typically use a dedicated <a href="https://thewirecutter.com/reviews/best-360-degree-camera/">360 camera</a>, which captures 2 images from fisheye lens at slightly more than 180 degrees and then merges the two images into one 360 image.<br />
However, there are also <a href="https://apps.apple.com/gb/app/google-street-view/id904418768">apps</a> that can stitch together a 360º panorama on device from a series of photos in different directions; and several drones have a <a href="https://store.dji.com/guides/spark-sphere-mode-review/">360 panorama capture mode</a>.</p>
<h1>How Do I View 360 Photos</h1>
<p><a href="https://spherium.app">Spherium</a> automatically shows you all the 360 photos in your iOS Photo Library and allows you to view them on your iPhone or iPad.</p>
iOS 13 UIContextMenu UIMenu UIAction
https://ikyle.me/blog/2017/ios13-uicontextmenu
2020-04-26T00:22:31.834634+00:00
2020-04-26T00:22:31.834626+00:00
Stand alone, UICollectionView and UITableView
<p>Example opening paragraph</p>
<h2>Example sub heading</h2>
<p>Example content.</p>
How to Add Chapters to MP4s with FFmpeg
https://ikyle.me/blog/2020/add-mp4-chapters-ffmpeg
2020-04-14T23:54:50.491259+00:00
2020-04-14T23:54:50.491259+00:00
How to add chapters to MP4s, with a helper python script to write the chapter metadata
<p>FFmpeg is one of those pieces of software which can do practically anything, <a href="https://ikyle.me/blog/2020/useful-ffmpeg-video-commands">if you can work out how</a>. One of the things it can do it modify the metadata for video files, including adding chapters.</p>
<p>Adding chapters is done via <a href="https://ffmpeg.org/ffmpeg-formats.html#Metadata-1">ffmetadata files</a>.</p>
<h2>Extract Metadata From Video</h2>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ffmpeg -i INPUT.mp4 -f ffmetadata FFMETADATAFILE.txt
</code></pre></div>
<p>Example output from a video file with no chapters.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">;FFMETADATA1</span>
<span style="color: #000000">title=None</span>
<span style="color: #000000">major_brand=isom</span>
<span style="color: #000000">minor_version=512</span>
<span style="color: #000000">compatible_brands=isomiso2avc1mp41</span>
<span style="color: #000000">encoder=Lavf57.56.100</span>
</code></pre></div>
<h2>Write Metadata To Video</h2>
<p>The metadata file can be edited, including to add chapters, and then ffmpeg can write it to a file.<br />
ffmpeg doesn't mutate the existing video, but instead creates a new video with the new metadata, existing video and existing audio.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ffmpeg -i INPUT.mp4 -i FFMETADATAFILE.txt -map_metadata <span style="color: #1C01CE">1</span> -codec copy OUTPUT.mp4
</code></pre></div>
<h2>Adding Chapters</h2>
<p>Adding chapters is done by editing the metadata file and adding sections like this:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">[CHAPTER]</span>
<span style="color: #836C28">TIMEBASE</span><span style="color: #000000">=</span><span style="color: #C41A16">1/1000</span>
<span style="color: #836C28">START</span><span style="color: #000000">=</span><span style="color: #C41A16">1</span>
<span style="color: #836C28">END</span><span style="color: #000000">=</span><span style="color: #C41A16">448000</span>
<span style="color: #836C28">title</span><span style="color: #000000">=</span><span style="color: #C41A16">The Pledge</span>
<span style="color: #A90D91">[CHAPTER]</span>
<span style="color: #836C28">TIMEBASE</span><span style="color: #000000">=</span><span style="color: #C41A16">1/1000</span>
<span style="color: #836C28">START</span><span style="color: #000000">=</span><span style="color: #C41A16">448001</span>
<span style="color: #836C28">END</span><span style="color: #000000">=</span> <span style="color: #C41A16">3883999</span>
<span style="color: #836C28">title</span><span style="color: #000000">=</span><span style="color: #C41A16">The Turn</span>
<span style="color: #A90D91">[CHAPTER]</span>
<span style="color: #836C28">TIMEBASE</span><span style="color: #000000">=</span><span style="color: #C41A16">1/1000</span>
<span style="color: #836C28">START</span><span style="color: #000000">=</span><span style="color: #C41A16">3884000</span>
<span style="color: #836C28">END</span><span style="color: #000000">=</span><span style="color: #C41A16">4418000</span>
<span style="color: #836C28">title</span><span style="color: #000000">=</span> <span style="color: #C41A16">The Prestige</span>
</code></pre></div>
<hr />
<h1>Helper Script</h1>
<p>The requirement to specify times as an integer of some time base, instead of the 1:30:20.5 style format usually used for displaying time is very off-putting to me, so I created a small helper script so I could just note down times while watching a video and have the metadata entries automatically worked out how my automatically.</p>
<p>The script has a few requirements:</p>
<ul>
<li>It expects to be in the same directory as a 'chapters.txt' file to read from, and an 'FFMETADATAFILE' file to output to.</li>
<li>It expects the chapters.txt file to be a txt file with 1 chapter per line starting with 0:00:00 style time stamps and followed by the title of the chapter</li>
<li>It appends to an existing FFMETADATAFILE</li>
<li>It expects no existing chapters in the file</li>
<li>It expects an final END chapter which is ignored, but provides the end point for the last real chapter.</li>
</ul>
<p>Example Chapters.txt</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #1C01CE">0</span><span style="color: #000000">:</span><span style="color: #1C01CE">23</span><span style="color: #000000">:</span><span style="color: #1C01CE">20</span> <span style="color: #000000">Start</span>
<span style="color: #1C01CE">0</span><span style="color: #000000">:</span><span style="color: #1C01CE">40</span><span style="color: #000000">:</span><span style="color: #1C01CE">30</span> <span style="color: #000000">First</span> <span style="color: #000000">Performance</span>
<span style="color: #1C01CE">0</span><span style="color: #000000">:</span><span style="color: #1C01CE">40</span><span style="color: #000000">:</span><span style="color: #1C01CE">56</span> <span style="color: #000000">Break</span>
<span style="color: #1C01CE">1</span><span style="color: #000000">:</span><span style="color: #1C01CE">04</span><span style="color: #000000">:</span><span style="color: #1C01CE">44</span> <span style="color: #000000">Second</span> <span style="color: #000000">Performance</span>
<span style="color: #1C01CE">1</span><span style="color: #000000">:</span><span style="color: #1C01CE">24</span><span style="color: #000000">:</span><span style="color: #1C01CE">45</span> <span style="color: #000000">Crowd</span> <span style="color: #000000">Shots</span>
<span style="color: #1C01CE">1</span><span style="color: #000000">:</span><span style="color: #1C01CE">27</span><span style="color: #000000">:</span><span style="color: #1C01CE">45</span> <span style="color: #000000">Credits</span>
</code></pre></div>
<p>Helper script</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">import</span> <span style="color: #000000">re</span>
<span style="color: #000000">chapters</span> <span style="color: #000000">=</span> <span style="color: #A90D91">list</span>()
<span style="color: #A90D91">with</span> <span style="color: #A90D91">open</span>(<span style="color: #C41A16">'chapters.txt'</span>, <span style="color: #C41A16">'r'</span>) <span style="color: #A90D91">as</span> <span style="color: #000000">f</span>:
<span style="color: #A90D91">for</span> <span style="color: #000000">line</span> <span style="color: #000000">in</span> <span style="color: #000000">f</span>:
<span style="color: #000000">x</span> <span style="color: #000000">=</span> <span style="color: #000000">re.match</span>(<span style="color: #C41A16">r"(\d):(\d{2}):(\d{2}) (.*)"</span>, <span style="color: #000000">line</span>)
<span style="color: #000000">hrs</span> <span style="color: #000000">=</span> <span style="color: #A90D91">int</span>(<span style="color: #000000">x.group</span>(<span style="color: #1C01CE">1</span>))
<span style="color: #000000">mins</span> <span style="color: #000000">=</span> <span style="color: #A90D91">int</span>(<span style="color: #000000">x.group</span>(<span style="color: #1C01CE">2</span>))
<span style="color: #000000">secs</span> <span style="color: #000000">=</span> <span style="color: #A90D91">int</span>(<span style="color: #000000">x.group</span>(<span style="color: #1C01CE">3</span>))
<span style="color: #000000">title</span> <span style="color: #000000">=</span> <span style="color: #000000">x.group</span>(<span style="color: #1C01CE">4</span>)
<span style="color: #000000">minutes</span> <span style="color: #000000">=</span> (<span style="color: #000000">hrs</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">60</span>) <span style="color: #000000">+</span> <span style="color: #000000">mins</span>
<span style="color: #000000">seconds</span> <span style="color: #000000">=</span> <span style="color: #000000">secs</span> <span style="color: #000000">+</span> (<span style="color: #000000">minutes</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">60</span>)
<span style="color: #000000">timestamp</span> <span style="color: #000000">=</span> (<span style="color: #000000">seconds</span> <span style="color: #000000">*</span> <span style="color: #1C01CE">1000</span>)
<span style="color: #000000">chap</span> <span style="color: #000000">=</span> {
<span style="color: #C41A16">"title"</span>: <span style="color: #000000">title</span>,
<span style="color: #C41A16">"startTime"</span>: <span style="color: #000000">timestamp</span>
}
<span style="color: #000000">chapters.append</span>(<span style="color: #000000">chap</span>)
<span style="color: #000000">text</span> <span style="color: #000000">=</span> <span style="color: #C41A16">""</span>
<span style="color: #A90D91">for</span> <span style="color: #000000">i</span> <span style="color: #000000">in</span> <span style="color: #A90D91">range</span>(<span style="color: #A90D91">len</span>(<span style="color: #000000">chapters</span>)<span style="color: #000000">-</span><span style="color: #1C01CE">1</span>):
<span style="color: #000000">chap</span> <span style="color: #000000">=</span> <span style="color: #000000">chapters</span>[<span style="color: #000000">i</span>]
<span style="color: #000000">title</span> <span style="color: #000000">=</span> <span style="color: #000000">chap</span>[<span style="color: #C41A16">'title'</span>]
<span style="color: #000000">start</span> <span style="color: #000000">=</span> <span style="color: #000000">chap</span>[<span style="color: #C41A16">'startTime'</span>]
<span style="color: #000000">end</span> <span style="color: #000000">=</span> <span style="color: #000000">chapters</span>[<span style="color: #000000">i+</span><span style="color: #1C01CE">1</span>][<span style="color: #C41A16">'startTime'</span>]<span style="color: #000000">-</span><span style="color: #1C01CE">1</span>
<span style="color: #000000">text</span> <span style="color: #000000">+=</span> <span style="color: #C41A16">f"""</span>
<span style="color: #C41A16">[CHAPTER]</span>
<span style="color: #C41A16">TIMEBASE=1/1000</span>
<span style="color: #C41A16">START={start}</span>
<span style="color: #C41A16">END={end}</span>
<span style="color: #C41A16">title={title}</span>
<span style="color: #C41A16">"""</span>
<span style="color: #A90D91">with</span> <span style="color: #A90D91">open</span>(<span style="color: #C41A16">"FFMETADATAFILE"</span>, <span style="color: #C41A16">"a"</span>) <span style="color: #A90D91">as</span> <span style="color: #000000">myfile</span>:
<span style="color: #000000">myfile.write</span>(<span style="color: #000000">text</span>)
</code></pre></div>
<p>The script could be modified to remove the need for a <code>chapters.txt</code> file by swapping line 5's <code>with. ...</code> statement for this.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">chapters_text</span> <span style="color: #000000">=</span> <span style="color: #C41A16">"""0:40:30 First Performance</span>
<span style="color: #C41A16">0:40:56 Break</span>
<span style="color: #C41A16">... etc ...</span>
<span style="color: #C41A16">"""</span>
<span style="color: #000000">f</span> <span style="color: #000000">=</span> <span style="color: #000000">chapters_text.splitlines</span>()
</code></pre></div>
<hr />
<h3>Example Usage With a Video With No Chapters</h3>
<p>Get existing video metadata</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ffmpeg -i INPUT.mp4 -f ffmetadata FFMETADATAFILE
</code></pre></div>
<p>Check there are no existing chapters.<br />
Watch the video, noting chapters into a <code>chapters.txt</code> file as you go.<br />
Place <code>FFMETADATAFILE</code>, <code>chapters.txt</code>, and the video file in the same directory.</p>
<p>Run the helper script to append chapters to FFMETADATAFILE.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>python3 helper.py
</code></pre></div>
<p>Create a new video, copying the video and audio from the original without re-encoding.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ffmpeg -i INPUT.mp4 -i FFMETADATAFILE -map_metadata <span style="color: #1C01CE">1</span> -codec copy OUTPUT.mp4
</code></pre></div>
Useful FFmpeg Commands for Sharing Videos
https://ikyle.me/blog/2020/useful-ffmpeg-video-commands
2020-04-27T20:40:55.314617+00:00
2020-04-07T17:14:37.597317+00:00
A collection of FFmpeg commands I find helpful when sharing videos
<p>A collection of FFmpeg commands I find helpful when working with and sharing videos.</p>
<h2>Convert to MP4</h2>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ffmpeg -i input.avi output.mp4
</code></pre></div>
<h2>Convert from MKV to MP4 Without Re-encoding</h2>
<p>This obviously only works the MKV contains a video that can be put in an MP4, like h264 or h265.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ffmpeg -i input.mkv -c copy output.mp4
</code></pre></div>
<p>Or you can specify the audio and video separately (and convert the audio by leaving it out, for example).</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ffmpeg -i input.mkv -vcodec copy -acodec copy output.mp4
ffmpeg -i input.mkv -vcodec copy output.mp4
</code></pre></div>
<h2>Change Framerate</h2>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ffmpeg -i input60fps.mp4 -r <span style="color: #1C01CE">30</span> output30fps.mp4
</code></pre></div>
<h2>Compress For Sharing On The Web</h2>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ffmpeg -i input.mp4 -c:v libx264 -crf <span style="color: #1C01CE">25</span> -c:a aac -movflags faststart output.mp4
</code></pre></div>
<p>The compression factor is set with <code>-crf</code>, the ffmpeg default for h264 is 23 and 28 for h265.</p>
<blockquote>
<p>With x264 and x265, you can set the values between 0 and 51, where lower values would result in better quality, at the expense of higher file sizes. Higher values mean more compression, but at some point you will notice the quality degradation.</p>
</blockquote>
<p>Quote for: <a href="https://slhck.info/video/2017/02/24/crf-guide.html">CRF Guide (Constant Rate Factor in x264, x265 and libvpx)</a></p>
<h2>Extract Part of Video</h2>
<p><code>-ss</code> to seek to a part of the video.<br />
<code>-t</code> for the duration, or <code>-to</code> for the time in the video</p>
<p>The behaviour depends on the position of the <code>-ss</code> parameter. These commands will extract the 10s from 0:30-0:40 in the original video.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ffmpeg -i in.mp4 -ss <span style="color: #1C01CE">30</span> -t <span style="color: #1C01CE">10</span> -c copy out.mp4
ffmpeg -i in.mp4 -ss <span style="color: #1C01CE">30</span> -to <span style="color: #1C01CE">40</span> -c copy out.mp4
</code></pre></div>
<p>However, if the seek is placed before the file input then timestamps are relative to the seek and these commands now behave differently from each other. With the first output being the same as the original commands, but the second now extracting the 40s long segment from 0:30 to 1:10 in the video.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ffmpeg -ss <span style="color: #1C01CE">30</span> -i in.mp4 -t <span style="color: #1C01CE">10</span> -c copy out.mp4
ffmpeg -ss <span style="color: #1C01CE">30</span> -i in.mp4 -to <span style="color: #1C01CE">40</span> -c copy out.mp4
</code></pre></div>
<ul>
<li><a href="https://superuser.com/a/377407/89838">How to cut a video - superuser</a></li>
<li><a href="https://trac.ffmpeg.org/wiki/Seeking">Seeking - FFmpeg wiki</a></li>
</ul>
<h2>Extract Audio From Video</h2>
<p>To extract audio from a video you just have to specify an audio file output and it'll ignore the video.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ffmpeg -i input.mkv output.mp3
</code></pre></div>
<p>To extract the raw audio from an MP4 without re-encoding (this depends on what codec the audio data actually is inside the mp4, for most H264 video mp4 files this is AAC audio).</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ffmpeg -i input.mp4 -c copy output.aac
</code></pre></div>
<h2>Extract Frame Image</h2>
<p>Good for quickly getting a thumbnail of the video.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">ffmpeg -ss 00:23:00 -i input.mp4 -frames:v 1 output.jpg</span>
<span style="color: #000000">ffmpeg -i input.mp4 -ss 00:00:10.5 -vframes 1 output.png</span>
</code></pre></div>
<ul>
<li><a href="https://superuser.com/a/608125/89838">Meaningful thumbnails for a Video using FFmpeg - superuser</a></li>
<li><a href="https://trac.ffmpeg.org/wiki/Create%20a%20thumbnail%20image%20every%20X%20seconds%20of%20the%20video">Create a thumbnail image every X seconds of the video - FFmpeg wiki</a></li>
</ul>
How to Add iOS 13.4 Device Support to Xcode 11.3 on Mojave
https://ikyle.me/blog/2020/add-device-support-to-xcode
2020-04-04T01:03:06.264285+00:00
2020-04-04T01:03:06.264285+00:00
Quick guide to adding support for unsupported iOS versions to Xcode by manually adding the needed DeviceSupport files
<p>The latest version of iOS (13.4) was recently released, and with it the new version of Xcode. This new version of Xcode only runs on macOS Catalina. If, like me, you have been avoiding Catalina as much as possible this is a problem, because if you try to use the last version of Xcode that supports Mojave (Xcode 11.3) to Build and Run on a device that is running iOS 13.4 it will give you an error (as it only has support for iOS versions up to iOS 13.3).</p>
<p>However, you can add extra iOS version support to Xcode by taking the DeviceSupport files from one version and adding them to another version (both to add support for newer, and older versions of iOS).</p>
<hr />
<ol>
<li><a href="https://github.com/iGhibli/iOS-DeviceSupport/tree/master/DeviceSupport">Find</a> or download <a href="https://developer.apple.com/download/more/?=xcode">a version of Xcode</a> that support the iOS version you want, in this case the latest one.</li>
<li>Extract the Xcode.app bundle, navigate to <code>Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/</code></li>
<li>Take the folder for the iOS version you need and place it in the equivalent directory inside the version of Xcode you are running.</li>
<li>At this point you'll need to restart Xcode at the minimum, though in my case I had to reboot completely.</li>
<li>Now connect the iOS device running that iOS version and attempt to Build and Run to it.</li>
<li>Xcode should not pull down the symbol files it needs from the device and you are good to go.</li>
</ol>
<p>This will not give you the new SDK, but should allow you to continue using your existing Xcode for a while longer.</p>
View Remote Server Logs Locally using GoAccess
https://ikyle.me/blog/2020/running-goaccess-locally
2020-04-02T03:33:41.875442+00:00
2020-04-02T03:33:41.875442+00:00
How to use a locally installed copy of GoAccess to view a remote server's access logs
<p>Since I removed Google Analytics I've had 0 visibility into my websites traffic, and I'd really like to at least see some stats like the page views.</p>
<p>My website is a mix of Nginx serving static files and forwarding dynamic pages to a python web server using Flask.<br />
After a bit of searching I realised I could just use Nginx's access logs directly, rather than introducing anything new. <a href="https://goaccess.io">GoAccess</a> seemed to be the most recommended tool, but I wanted to avoid installing anything new on the server if I could help it, so I was really looking for a local tool.</p>
<p>Fortunately, it is possible to run GoAccess on your local machine and view the server logs remotely over SSH.</p>
<h1>How To</h1>
<p>First make sure the server logs are accessible to the user you will be SSH'ing in with, if they aren't then SSH in and fix the permissions first: <code>sudo chmod -R +r /var/log/nginx/</code>.</p>
<p>On macOS installing GoAccess is as simple as running:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">brew install goaccess</span>
</code></pre></div>
<p>Then to view a single log file you can ssh into the server, cat the log file and pipe the output directly to GoAccess on your local machine.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ssh USER@IP_ADDRESS <span style="color: #C41A16">'cat /var/log/nginx/access.log'</span> | goaccess -a --log-format<span style="color: #000000">=</span>COMBINED
</code></pre></div>
<p>After a while nginx will compress the log files and start again so you end up with a directory looking like:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">access.log access.log.2.gz access.log.3.gz</span>
<span style="color: #000000">access.log.4.gz access.log.5.gz access.log.6.gz</span>
</code></pre></div>
<p>There are now multiple log files, most of which are compressed.</p>
<p>To view all the log files you can run <a href="https://stackoverflow.com/a/39240021/458205">this command</a> to uncompress them, and concatenate them together before feeding them into GoAccess.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>ssh USER@IP_ADDRESS <span style="color: #C41A16">'zcat -f /var/log/nginx/access.log*'</span> | goaccess --log-format<span style="color: #000000">=</span>COMBINED
</code></pre></div>
<p>More details about actually using GoAccess to navigate the logs you have opened can be found on <a href="https://goaccess.io/man#examples">GoAccess's website</a>.</p>
Using UIImagePickerController on iOS
https://ikyle.me/blog/2020/uiimagepickercontroller
2020-04-01T00:52:13.995134+00:00
2020-04-01T00:52:13.995134+00:00
How to use UIImagePickerController to select a photo from the Photo Library on iOS in Objective C and Swift
<p>The simplest way to allow users to select a photo is to use the <a href="https://developer.apple.com/documentation/uikit/uiimagepickercontroller"><code>UIImagePickerController</code></a> class, which provides a built in UI for users to access their photos.</p>
<p>Doing so is incredibly simple.</p>
<ol>
<li>Declare that your class conforms to the <code>UIImagePickerControllerDelegate</code> and <code>UINavigationControllerDelegate</code> protocols.</li>
<li>Implement the <code>-imagePickerController:didFinishPickingMediaWithInfo:</code> method.</li>
<li>Create and present an <code>UIImagePickerController</code> view controller class.</li>
<li>Do something with the image returned</li>
</ol>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>- (<span style="color: #A90D91">void</span>)<span style="color: #000000">pickPhoto</span> {
<span style="color: #5B269A">UIImagePickerController</span> <span style="color: #000000">*imagePicker</span> <span style="color: #000000">=</span> [[<span style="color: #5B269A">UIImagePickerController</span> <span style="color: #000000">alloc</span>] <span style="color: #000000">init</span>];
<span style="color: #000000">imagePicker</span>.<span style="color: #000000">sourceType</span> <span style="color: #000000">=</span> <span style="color: #000000">UIImagePickerControllerSourceTypePhotoLibrary</span>;
<span style="color: #000000">imagePicker</span>.<span style="color: #000000">delegate</span> <span style="color: #000000">=</span> <span style="color: #A90D91">self</span>;
[<span style="color: #A90D91">self</span> <span style="color: #000000">presentViewController</span>:<span style="color: #000000">imagePicker</span> <span style="color: #000000">animated</span>:<span style="color: #A90D91">YES</span> <span style="color: #000000">completion</span>:<span style="color: #A90D91">nil</span>];
}
<span style="color: #177500">// Implement UIImagePickerControllerDelegate method</span>
-(<span style="color: #A90D91">void</span>)<span style="color: #000000">imagePickerController:</span>(<span style="color: #5B269A">UIImagePickerController</span> <span style="color: #000000">*</span>)<span style="color: #000000">picker</span> <span style="color: #000000">didFinishPickingMediaWithInfo:</span>(<span style="color: #5B269A">NSDictionary</span><span style="color: #000000"><UIImagePickerControllerInfoKey</span>,<span style="color: #A90D91">id</span><span style="color: #000000">></span> <span style="color: #000000">*</span>)<span style="color: #000000">info</span>{
<span style="color: #5B269A">UIImage</span> <span style="color: #000000">*image</span> <span style="color: #000000">=</span> <span style="color: #000000">info</span>[<span style="color: #000000">UIImagePickerControllerOriginalImage</span>];
<span style="color: #A90D91">self</span>.<span style="color: #000000">imageView</span>.<span style="color: #000000">image</span> <span style="color: #000000">=</span> <span style="color: #000000">image</span>;
[<span style="color: #000000">picker</span> <span style="color: #000000">dismissViewControllerAnimated</span>:<span style="color: #A90D91">YES</span> <span style="color: #000000">completion</span>:<span style="color: #A90D91">nil</span>];
}
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">func</span> <span style="color: #000000">pickPhoto</span>() {
<span style="color: #A90D91">let</span> <span style="color: #000000">imagePicker</span> = <span style="color: #5B269A">UIImagePickerController</span>()
<span style="color: #000000">imagePicker</span>.<span style="color: #000000">sourceType</span> = .<span style="color: #000000">photoLibrary</span>
<span style="color: #000000">imagePicker</span>.<span style="color: #000000">delegate</span> = <span style="color: #A90D91">self</span>
<span style="color: #000000">present</span>(<span style="color: #000000">imagePicker</span>, <span style="color: #000000">animated</span>: <span style="color: #A90D91">true</span>)
}
<span style="color: #177500">// Implement UIImagePickerControllerDelegate method</span>
<span style="color: #A90D91">public</span> <span style="color: #A90D91">func</span> <span style="color: #000000">imagePickerController</span>(<span style="color: #A90D91">_</span> <span style="color: #000000">picker</span>: <span style="color: #5B269A">UIImagePickerController</span>, <span style="color: #000000">didFinishPickingMediaWithInfo</span> <span style="color: #000000">info</span>: [<span style="color: #5B269A">UIImagePickerController</span>.<span style="color: #000000">InfoKey</span>: <span style="color: #A90D91">Any</span>])
<span style="color: #000000">imageView</span>.<span style="color: #000000">image</span> = (<span style="color: #000000">info</span>[.<span style="color: #000000">originalImage</span>] <span style="color: #A90D91">as</span>? <span style="color: #5B269A">UIImage</span>)
<span style="color: #000000">picker</span>.<span style="color: #000000">dismiss</span>(<span style="color: #000000">animated</span>: <span style="color: #A90D91">true</span>, <span style="color: #000000">completion</span>: <span style="color: #A90D91">nil</span>)
}
</code></pre></div>
<h1>Footnotes</h1>
<h2>UIImagePickerControllerSourceType</h2>
<ul>
<li><code>UIImagePickerControllerSourceTypePhotoLibrary</code> (<code>.photoLibrary</code>) allows users to pick from their full photo library, including browsing their albums.</li>
<li><code>UIImagePickerControllerSourceTypeSavedPhotosAlbum</code> (<code>.savedPhotosAlbum</code>) limits the selection to just their chronological 'Moments' screen, camera roll.</li>
<li><code>UIImagePickerControllerSourceTypeCamera</code> (<code>.camera</code>) shows a camera interface so the user can take a picture their and then in the app.</li>
</ul>
<p><strong>Note:</strong> using <code>UIImagePickerControllerSourceTypeCamera</code> (<code>.camera</code>) will crash the app unless the info.plist contains the <code>NSCameraUsageDescription</code> key with the following error message; unlike the other two, which use the act of the user selecting a photo as implied consent to give that photo too the app, and do not prompt for permission <a href="https://stackoverflow.com/a/46594741/458205">since iOS 11, when it was changed</a> to run as a separate process (and so no longer require the <code>NSPhotoLibraryUsageDescription</code> key).</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">[access]</span> <span style="color: #000000">This</span> <span style="color: #000000">app</span> <span style="color: #000000">has</span> <span style="color: #000000">crashed</span> <span style="color: #000000">because</span> <span style="color: #000000">it</span> <span style="color: #000000">attempted</span> <span style="color: #A90D91">to</span> <span style="color: #000000">access</span> <span style="color: #000000">privacy-</span><span style="color: #A90D91">sensitive</span> <span style="color: #A90D91">data</span> <span style="color: #A90D91">without</span> <span style="color: #000000">a</span> <span style="color: #A90D91">usage</span> <span style="color: #000000">description</span>.
<span style="color: #000000">The</span> <span style="color: #000000">app's</span> <span style="color: #000000">Info</span>.<span style="color: #000000">plist</span> <span style="color: #000000">must</span> <span style="color: #000000">contain</span> <span style="color: #000000">an</span> <span style="color: #000000">NSCameraUsageDescription</span> <span style="color: #A90D91">key</span> <span style="color: #A90D91">with</span> <span style="color: #000000">a</span> <span style="color: #000000">string</span> <span style="color: #A90D91">value</span> <span style="color: #000000">explaining</span> <span style="color: #A90D91">to</span> <span style="color: #000000">the</span> <span style="color: #A90D91">user</span> <span style="color: #000000">how</span> <span style="color: #000000">the</span> <span style="color: #000000">app</span> <span style="color: #000000">uses</span> <span style="color: #000000">this</span> <span style="color: #A90D91">data</span>.
</code></pre></div>
<h2>Select Photos, Videos or Both</h2>
<p>By default <code>UIImagePickerController</code> will only show and select images from a users photo library. However this is actually controlled via the <code>mediaTypes</code> property, which acts as a filter. To select photos and videos you would set the array to include videos.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">@import</span> <span style="color: #000000">MobileCoreServices</span>; <span style="color: #177500">// where kUTTypeImage and kUTTypeMovie are defined</span>
<span style="color: #000000">imagePicker</span>.<span style="color: #000000">mediaTypes</span> <span style="color: #000000">=</span> <span style="color: #1C01CE">@[</span>(<span style="color: #5B269A">NSString</span><span style="color: #000000">*</span>)<span style="color: #000000">kUTTypeImage</span>, (<span style="color: #5B269A">NSString</span><span style="color: #000000">*</span>)<span style="color: #000000">kUTTypeMovie</span><span style="color: #1C01CE">]</span>;
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">import</span> <span style="color: #000000">MobileCoreServices</span>
<span style="color: #000000">imagePicker.mediaTypes</span> <span style="color: #000000">=</span> [(<span style="color: #000000">kUTTypeImage</span> <span style="color: #A90D91">as</span> <span style="color: #000000">String</span>), (<span style="color: #000000">kUTTypeMovie</span> <span style="color: #A90D91">as</span> <span style="color: #000000">String</span>)]
</code></pre></div>
<p>To extract the video you'll need to get the <code>UIImagePickerControllerMediaURL</code> (<code>.mediaURL</code>) key from the returning info dictionary, instead of the <code>UIImagePickerControllerOriginalImage</code> (<code>originalImage</code>) value.</p>
<p><strong>NOTE:</strong> The <a href="https://developer.apple.com/documentation/uikit/uiimagepickercontroller/1619173-mediatypes">developer documentation</a> for the property includes this interesting comment regarding those Live Photos that you've enabled effects like 'Bounce and Loop' on.<br />
<code>If you want to display a Live Photo rendered as a Loop or a Bounce, you must include the kUTTypeMovie identifier.</code></p>
<h2>Checking Capabilities</h2>
<p>The <code>UIImagePickerController</code> also includes several class methods to check availability of different features.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">if</span> ([<span style="color: #5B269A">UIImagePickerController</span> <span style="color: #000000">isSourceTypeAvailable</span>:<span style="color: #000000">UIImagePickerControllerSourceTypeCamera</span>]) {
<span style="color: #177500">// This device has a camera</span>
}
<span style="color: #5B269A">NSArray</span> <span style="color: #000000">*mediaTypes</span> <span style="color: #000000">=</span> [<span style="color: #5B269A">UIImagePickerController</span> <span style="color: #000000">availableMediaTypesForSourceType</span>:<span style="color: #000000">UIImagePickerControllerSourceTypeCamera</span>];
<span style="color: #177500">// Can this camera capture video, or just Photos (i.e: the first iPhone vs the iPhone 3G)</span>
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">if</span> <span style="color: #5B269A">UIImagePickerController</span>.<span style="color: #000000">isSourceTypeAvailable</span>(.<span style="color: #000000">camera</span>) {
<span style="color: #177500">// This device has a camera</span>
}
<span style="color: #A90D91">let</span> <span style="color: #000000">mediaTypes</span>:[<span style="color: #A90D91">String</span>]? = <span style="color: #5B269A">UIImagePickerController</span>.<span style="color: #000000">availableMediaTypes</span>(<span style="color: #A90D91">for</span>: .<span style="color: #000000">camera</span>)
<span style="color: #177500">// Can this camera capture video, or just Photos (i.e: the first iPhone vs the iPhone 3G)</span>
</code></pre></div>
<h2>Other</h2>
<p>There are also more APIs, such as for getting the availability of flash, checking front and back cameras availability, checking the capabilities of each camera separately, and for allowing basic editing of the photo before it is given to your app. <a href="https://developer.apple.com/documentation/uikit/uiimagepickercontroller">Official UIImagePickerController Class Documentation</a></p>
<h1>Limitations</h1>
<p>UIImagePickerController is an incredibly useful and convenient class for quickly adding image selection into an app. But it does have limitations:</p>
<ul>
<li>no multi-selection support</li>
<li>limited filtering options (only photos, or videos, or both)</li>
<li>no direct programmatic access to the photo library (only the photo you are given)</li>
<li>limited camera access or UI customization.</li>
</ul>
<p>If you want any of those features then you'll need to use <code>AVFoundation</code> to make a custom camera UI, or <code>PhotoKit</code> (which will be the subject of the next blog post) to make a custom image picker.</p>
QRCode Generation and Scanning on iOS 13
https://ikyle.me/blog/2020/ios-qr-codes
2020-03-11T00:06:48.117661+00:00
2020-03-11T00:06:48.117661+00:00
How to generate a QR code image and how to scan a QR code with camera on iOS
<h1>Generating QRCodes using CoreImage</h1>
<p>iOS has built in support for generating <a href="https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/uid/TP30000136-SW142">several different</a> types of 1D and 2D bar codes.</p>
<p>The API in the <code>CoreImage</code> framework used to generate these codes is <code>CIFilter</code>, which returns a <code>CIImage</code>. We can then turn this into a <code>UIImage</code> to display on screen otherwise use.</p>
<p>The filter is given an <code>NSData</code> object as the input, and a string representing the amount of error correction to be used when making the QRCode (<code>L</code> 7%, <code>M</code> 15%, <code>Q</code> 25%, or <code>H</code> 30%).</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">let</span> <span style="color: #000000">qrFilter</span> = <span style="color: #5B269A">CIFilter</span>(<span style="color: #000000">name</span>: <span style="color: #C41A16">"CIQRCodeGenerator"</span>)
<span style="color: #177500">// Text</span>
<span style="color: #000000">qrFilter</span>?.<span style="color: #000000">setValue</span>(<span style="color: #000000">text</span>.<span style="color: #000000">data</span>(<span style="color: #000000">using</span>: .<span style="color: #000000">isoLatin1</span>), <span style="color: #000000">forKey</span>: <span style="color: #C41A16">"inputMessage"</span>)
<span style="color: #177500">// Error correction</span>
<span style="color: #000000">qrFilter</span>?.<span style="color: #000000">setValue</span>(<span style="color: #C41A16">"M"</span>, <span style="color: #000000">forKey</span>: <span style="color: #C41A16">"inputCorrectionLevel"</span>)
<span style="color: #A90D91">return</span> <span style="color: #000000">qrFilter</span>?.<span style="color: #000000">outputImage</span>
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #5B269A">CIFilter</span> <span style="color: #000000">*qrFilter</span> <span style="color: #000000">=</span> [<span style="color: #5B269A">CIFilter</span> <span style="color: #000000">filterWithName</span>:<span style="color: #C41A16">@"CIQRCodeGenerator"</span>];
<span style="color: #177500">// Set the QR Code contents</span>
[<span style="color: #000000">qrFilter</span> <span style="color: #000000">setValue</span>:[<span style="color: #000000">qrString</span> <span style="color: #000000">dataUsingEncoding</span>:<span style="color: #000000">NSISOLatin1StringEncoding</span>] <span style="color: #000000">forKey</span>:<span style="color: #C41A16">@"inputMessage"</span>];
<span style="color: #177500">// Error correction</span>
[<span style="color: #000000">qrFilter</span> <span style="color: #000000">setValue</span>:<span style="color: #C41A16">@"M"</span> <span style="color: #000000">forKey</span>:<span style="color: #C41A16">@"inputCorrectionLevel"</span>];
<span style="color: #A90D91">return</span> <span style="color: #000000">qrFilter</span>.<span style="color: #000000">outputImage</span>;
</code></pre></div>
<p>One small oddity is that, unlike might be expected, the string is not encoded with UTF8, but instead <code>ISO Latin 1</code> string encoding.</p>
<h2>Scaling</h2>
<p>The resulting image is a QRCode, but is only as big as it needs to be so when you put it in your image view it will be scaled up and look blurry. So before using it you likely want to scale up the <code>CIImage</code> to the size of the image view that it will be displayed in.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">let</span> <span style="color: #000000">qrCode</span>:<span style="color: #5B269A">CIImage</span> = <span style="color: #000000">createQRCode</span>()
<span style="color: #A90D91">let</span> <span style="color: #000000">viewWidth</span> = <span style="color: #000000">imageView</span>.<span style="color: #000000">bounds</span>.<span style="color: #000000">size</span>.<span style="color: #000000">width</span>;
<span style="color: #A90D91">let</span> <span style="color: #000000">scale</span> = <span style="color: #000000">viewWidth/qrCode</span>.<span style="color: #000000">extent</span>.<span style="color: #000000">size</span>.<span style="color: #000000">width</span>;
<span style="color: #A90D91">let</span> <span style="color: #000000">scaledImage</span> = <span style="color: #000000">qrCode</span>.<span style="color: #000000">transformed</span>(<span style="color: #000000">by</span>: <span style="color: #5B269A">CGAffineTransform</span>(<span style="color: #000000">scaleX</span>: <span style="color: #000000">scale</span>, <span style="color: #000000">y</span>: <span style="color: #000000">scale</span>))
<span style="color: #000000">imageView</span>.<span style="color: #000000">image</span> = <span style="color: #5B269A">UIImage</span>(<span style="color: #000000">ciImage</span>: <span style="color: #000000">scaledImage</span>)
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #5B269A">CIImage</span> <span style="color: #000000">*qrCode</span> <span style="color: #000000">=</span> [<span style="color: #A90D91">self</span> <span style="color: #000000">createQRForString</span>:<span style="color: #000000">contents</span>];
<span style="color: #000000">CGFloat</span> <span style="color: #000000">viewWidth</span> <span style="color: #000000">=</span> <span style="color: #A90D91">self</span>.<span style="color: #000000">imageView</span>.<span style="color: #000000">bounds</span>.<span style="color: #000000">size</span>.<span style="color: #000000">width</span>;
<span style="color: #000000">CGFloat</span> <span style="color: #000000">scale</span> <span style="color: #000000">=</span> <span style="color: #000000">viewWidth/qrCode</span>.<span style="color: #000000">extent</span>.<span style="color: #000000">size</span>.<span style="color: #000000">width</span>;
<span style="color: #5B269A">CIImage</span> <span style="color: #000000">*scaledImage</span> <span style="color: #000000">=</span> [<span style="color: #000000">qrCode</span> <span style="color: #000000">imageByApplyingTransform</span>:<span style="color: #000000">CGAffineTransformMakeScale</span>(<span style="color: #000000">scale</span>, <span style="color: #000000">scale</span>)];
<span style="color: #A90D91">self</span>.<span style="color: #000000">imageView</span>.<span style="color: #000000">image</span> <span style="color: #000000">=</span> [<span style="color: #5B269A">UIImage</span> <span style="color: #000000">imageWithCIImage</span>:<span style="color: #000000">scaledImage</span>];
</code></pre></div>
<h1>Reading QRCodes with AVFoundation Metadata</h1>
<p>Reading a QRCode is similarly easy on iOS. When setting up a camera capture session (getting output from the camera) you can also attach various metadata outputs, for things such as faces, certain objects and QRCodes.</p>
<p>After getting a configured <code>AVCaptureSession</code> we can add our metadata output.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">let</span> <span style="color: #000000">metadataOutput</span> = <span style="color: #5B269A">AVCaptureMetadataOutput</span>()
<span style="color: #A90D91">if</span> (<span style="color: #000000">captureSession</span>.<span style="color: #000000">canAddOutput</span>(<span style="color: #000000">metadataOutput</span>)) {
<span style="color: #000000">captureSession</span>.<span style="color: #000000">addOutput</span>(<span style="color: #000000">metadataOutput</span>)
<span style="color: #000000">metadataOutput</span>.<span style="color: #000000">setMetadataObjectsDelegate</span>(<span style="color: #A90D91">self</span>, <span style="color: #000000">queue</span>: <span style="color: #000000">DispatchQueue</span>.<span style="color: #000000">main</span>)
<span style="color: #000000">metadataOutput</span>.<span style="color: #000000">metadataObjectTypes</span> = [.<span style="color: #000000">qr</span>]
}
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #5B269A">AVCaptureMetadataOutput</span> <span style="color: #000000">*metadataOutput</span> <span style="color: #000000">=</span> [[<span style="color: #5B269A">AVCaptureMetadataOutput</span> <span style="color: #000000">alloc</span>] <span style="color: #000000">init</span>];
<span style="color: #A90D91">if</span> ([<span style="color: #000000">captureSession</span> <span style="color: #000000">canAddOutput</span>:<span style="color: #000000">metadataOutput</span>]) {
[<span style="color: #000000">captureSession</span> <span style="color: #000000">addOutput</span>:<span style="color: #000000">metadataOutput</span>];
[<span style="color: #000000">metadataOutput</span> <span style="color: #000000">setMetadataObjectsDelegate</span>:<span style="color: #A90D91">self</span> <span style="color: #000000">queue</span>:<span style="color: #000000">dispatch_get_main_queue</span>()];
<span style="color: #000000">metadataOutput</span>.<span style="color: #000000">metadataObjectTypes</span> <span style="color: #000000">=</span> <span style="color: #1C01CE">@[</span><span style="color: #000000">AVMetadataObjectTypeQRCode</span><span style="color: #1C01CE">]</span>;
}
</code></pre></div>
<p>Then we just need to implement <code>AVCaptureMetadataOutputObjectsDelegate</code> and we have the QRCode's contents</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">func</span> <span style="color: #000000">metadataOutput</span>(<span style="color: #A90D91">_</span> <span style="color: #000000">output</span>: <span style="color: #5B269A">AVCaptureMetadataOutput</span>, <span style="color: #000000">didOutput</span> <span style="color: #000000">metadataObjects</span>: [<span style="color: #5B269A">AVMetadataObject</span>], <span style="color: #000000">from</span> <span style="color: #000000">connection</span>: <span style="color: #5B269A">AVCaptureConnection</span>) {
<span style="color: #A90D91">if</span> <span style="color: #A90D91">let</span> <span style="color: #000000">metadataObject</span> = <span style="color: #000000">metadataObjects</span>.<span style="color: #5B269A">first</span> {
<span style="color: #A90D91">guard</span> <span style="color: #A90D91">let</span> <span style="color: #000000">readableObject</span> = <span style="color: #000000">metadataObject</span> <span style="color: #A90D91">as</span>? <span style="color: #5B269A">AVMetadataMachineReadableCodeObject</span> <span style="color: #A90D91">else</span> { <span style="color: #A90D91">return</span> }
<span style="color: #A90D91">guard</span> <span style="color: #A90D91">let</span> <span style="color: #000000">stringValue</span> = <span style="color: #000000">readableObject</span>.<span style="color: #000000">stringValue</span> <span style="color: #A90D91">else</span> { <span style="color: #A90D91">return</span> }
<span style="color: #5B269A">print</span>(<span style="color: #C41A16">"QR Code: \(</span><span style="color: #000000">stringValue</span><span style="color: #C41A16">)"</span>)
}
}
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>- (<span style="color: #A90D91">void</span>)<span style="color: #000000">captureOutput:</span>(<span style="color: #5B269A">AVCaptureOutput</span> <span style="color: #000000">*</span>)<span style="color: #000000">captureOutput</span> <span style="color: #000000">didOutputMetadataObjects:</span>(<span style="color: #5B269A">NSArray</span> <span style="color: #000000">*</span>)<span style="color: #000000">metadataObjects</span> <span style="color: #000000">fromConnection:</span>(<span style="color: #5B269A">AVCaptureConnection</span> <span style="color: #000000">*</span>)<span style="color: #000000">connection</span>{
<span style="color: #5B269A">AVMetadataObject</span> <span style="color: #000000">*metadata</span> <span style="color: #000000">=</span> [<span style="color: #000000">metadataObjects</span> <span style="color: #000000">firstObject</span>];
<span style="color: #A90D91">if</span> ([<span style="color: #000000">metadata</span> <span style="color: #000000">isKindOfClass</span>:[<span style="color: #5B269A">AVMetadataMachineReadableCodeObject</span> <span style="color: #A90D91">class</span>]]) {
<span style="color: #5B269A">NSString</span> <span style="color: #000000">*stringValue</span> <span style="color: #000000">=</span> [(<span style="color: #5B269A">AVMetadataMachineReadableCodeObject</span> <span style="color: #000000">*</span>)<span style="color: #000000">metadata</span> <span style="color: #000000">stringValue</span>];
<span style="color: #000000">NSLog</span>(<span style="color: #C41A16">@"QR Code: %@"</span>, <span style="color: #000000">stringValue</span>);
}
}
</code></pre></div>
<p>You can also ask the camera preview layer to translate the detected object into it's local coordinate system, allowing you to overlay something on the camera view that matches the QRCode's position.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">let</span> <span style="color: #000000">qrCodeObject</span> = <span style="color: #000000">previewLayer</span>.<span style="color: #000000">transformedMetadataObject</span>(<span style="color: #A90D91">for</span>: <span style="color: #000000">readableObject</span>)
<span style="color: #000000">showQRCodeBounds</span>(<span style="color: #000000">frame</span>: <span style="color: #000000">qrCodeObject</span>?.<span style="color: #000000">bounds</span>)
</code></pre></div>
<h1>QRCode Generator and Scanner Example Project</h1>
<p>The example code in this blog post comes from an <a href="https://github.com/kylehowells/QR-Code-Generator-and-Scanner">example project on Github</a>, which I wrote to demonstrate how to generate a QR code image and how to scan a QR code with the devices camera. (There is also a less feature complete <a href="https://github.com/kylehowells/QR-Code-Demo-App/blob/master/QR%20Code%20Generator/ViewController.m">ObjC example project</a>)</p>
<table>
<thead>
<tr>
<th align="center">GeneratorViewController.swift</th>
<th align="center">ReaderViewController.swift</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><img src="https://ikyle.me/blog/2020/ios-qr-codes/Gen.jpeg" alt="" /></td>
<td align="center"><img src="https://ikyle.me/blog/2020/ios-qr-codes/Scan.jpeg" alt="" /></td>
</tr>
</tbody>
</table>
<h2>Project Structure</h2>
<p>The Xcode project uses the tab based application template, with the application and scene delegates unchanged from the Xcode template.<br />
The 2 main files are <a href="https://github.com/kylehowells/QR-Code-Generator-and-Scanner/blob/master/QR%20Codes/GeneratorViewController.swift"><code>GeneratorViewController.swift</code></a> and <a href="https://github.com/kylehowells/QR-Code-Generator-and-Scanner/blob/master/QR%20Codes/ReaderViewController.swift"><code>ReaderViewController.swift</code></a> and the code inside is very similar to the included example code in this blog post, just with the extra surrounding code needed to glue it together into a usable application.</p>
iOS Multiple Window Support Without Storyboards
https://ikyle.me/blog/2020/ios-multi-window-support-without-storyboard
2020-03-10T23:18:48.695121+00:00
2020-03-10T23:18:48.695121+00:00
Supporting multiple windows and the UIScene lifecycle API programtically
<p>If, like me, you prefer doing things programatically, instead of using interface builder, then the <code>Main.Storyboard</code> file doesn't last beyond creating the new project.</p>
<p>However, now with iOS 13 and multiple window there is now a <code>SceneDelegate</code> as well as an <code>AppDelegate</code> to deal with.<br />
Fortunately, it isn't much more complex than before, just moved around slightly.</p>
<h1>AppDelegate (iOS 12)</h1>
<p>With iOS 12 you would setup an application programatically by deleting the <code>Main.Storyboard</code> and then creating the window in the <code>AppDelegate</code>'s <code>application:willFinishLaunchingWithOptions:</code> or <code>application:didFinishLaunchingWithOptions:</code>.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">func</span> <span style="color: #000000">application</span>(<span style="color: #A90D91">_</span> <span style="color: #000000">application</span>: <span style="color: #5B269A">UIApplication</span>, <span style="color: #000000">didFinishLaunchingWithOptions</span> <span style="color: #000000">launchOptions</span>: [<span style="color: #5B269A">UIApplication</span>.<span style="color: #000000">LaunchOptionsKey</span>: <span style="color: #A90D91">Any</span>]?) -> <span style="color: #A90D91">Bool</span> {
<span style="color: #A90D91">self</span>.<span style="color: #000000">window</span> = <span style="color: #5B269A">UIWindow</span>(<span style="color: #000000">frame</span>: <span style="color: #5B269A">UIScreen</span>.<span style="color: #000000">main</span>.<span style="color: #000000">bounds</span>)
<span style="color: #A90D91">self</span>.<span style="color: #000000">window</span>.<span style="color: #000000">rootViewController</span> = <span style="color: #000000">ViewController</span>()
<span style="color: #A90D91">self</span>.<span style="color: #000000">window</span>.<span style="color: #000000">makeKeyAndVisible</span>()
<span style="color: #A90D91">return</span> <span style="color: #A90D91">true</span>
}
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>- (<span style="color: #A90D91">BOOL</span>)<span style="color: #000000">application:</span>(<span style="color: #5B269A">UIApplication</span> <span style="color: #000000">*</span>)<span style="color: #000000">application</span> <span style="color: #000000">didFinishLaunchingWithOptions:</span>(<span style="color: #5B269A">NSDictionary</span> <span style="color: #000000">*</span>)<span style="color: #000000">launchOptions</span> {
<span style="color: #A90D91">self</span>.<span style="color: #000000">window</span> <span style="color: #000000">=</span> [[<span style="color: #5B269A">UIWindow</span> <span style="color: #000000">alloc</span>] <span style="color: #000000">initWithFrame</span>:[[<span style="color: #5B269A">UIScreen</span> <span style="color: #000000">mainScreen</span>] <span style="color: #000000">bounds</span>]];
<span style="color: #A90D91">self</span>.<span style="color: #000000">window</span>.<span style="color: #000000">rootViewController</span> <span style="color: #000000">=</span> [[<span style="color: #000000">ViewController</span> <span style="color: #000000">alloc</span>] <span style="color: #000000">init</span>];
[<span style="color: #A90D91">self</span>.<span style="color: #000000">window</span> <span style="color: #000000">makeKeyAndVisible</span>];
<span style="color: #A90D91">return</span> <span style="color: #A90D91">YES</span>;
}
</code></pre></div>
<h1>SceneDelegate (iOS 13)</h1>
<p>The <code>info.plist</code> file still has the <code>UIMainStoryboardFile</code> key-value pair, but now it also has a <code>UISceneStoryboardFile</code> key inside the <code>UIApplicationSceneManifest -> UISceneConfigurations</code> array.</p>
<p>Once those 2 are deleted, the Storyboard is no longer involved in creating your apps UI and you need to do that in code yourself.<br />
The location to do so is now in the <code>SceneDelegate</code>'s <code>-scene:willConnectToSession:options:</code> method.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">func</span> <span style="color: #000000">scene</span>(<span style="color: #A90D91">_</span> <span style="color: #000000">scene</span>: <span style="color: #000000">UIScene</span>, <span style="color: #000000">willConnectTo</span> <span style="color: #000000">session</span>: <span style="color: #000000">UISceneSession</span>, <span style="color: #000000">options</span> <span style="color: #000000">connectionOptions</span>: <span style="color: #000000">UIScene</span>.<span style="color: #000000">ConnectionOptions</span>) {
<span style="color: #177500">// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.</span>
<span style="color: #A90D91">guard</span> <span style="color: #A90D91">let</span> <span style="color: #000000">windowScene</span> = (<span style="color: #000000">scene</span> <span style="color: #A90D91">as</span>? <span style="color: #000000">UIWindowScene</span>) <span style="color: #A90D91">else</span> { <span style="color: #A90D91">return</span> }
<span style="color: #A90D91">let</span> <span style="color: #000000">window</span> = <span style="color: #5B269A">UIWindow</span>(<span style="color: #000000">windowScene</span>: <span style="color: #000000">windowScene</span>)
<span style="color: #000000">window</span>.<span style="color: #000000">rootViewController</span> = <span style="color: #000000">ViewController</span>()
<span style="color: #A90D91">self</span>.<span style="color: #000000">window</span> = <span style="color: #000000">window</span>
<span style="color: #000000">window</span>.<span style="color: #000000">makeKeyAndVisible</span>()
}
</code></pre></div>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>- (<span style="color: #A90D91">void</span>)<span style="color: #000000">scene:</span>(<span style="color: #000000">UIScene</span> <span style="color: #000000">*</span>)<span style="color: #000000">scene</span> <span style="color: #000000">willConnectToSession:</span>(<span style="color: #000000">UISceneSession</span> <span style="color: #000000">*</span>)<span style="color: #000000">session</span> <span style="color: #000000">options:</span>(<span style="color: #000000">UISceneConnectionOptions</span> <span style="color: #000000">*</span>)<span style="color: #000000">connectionOptions</span> {
<span style="color: #A90D91">self</span>.<span style="color: #000000">window</span> <span style="color: #000000">=</span> [[<span style="color: #5B269A">UIWindow</span> <span style="color: #000000">alloc</span>] <span style="color: #000000">initWithWindowScene</span>:(<span style="color: #000000">UIWindowScene*</span>)<span style="color: #000000">scene</span>];
<span style="color: #A90D91">self</span>.<span style="color: #000000">window</span>.<span style="color: #000000">rootViewController</span> <span style="color: #000000">=</span> [[<span style="color: #000000">ViewController</span> <span style="color: #000000">alloc</span>] <span style="color: #000000">init</span>];
[<span style="color: #A90D91">self</span>.<span style="color: #000000">window</span> <span style="color: #000000">makeKeyAndVisible</span>];
}
</code></pre></div>
<p>The only difference compared to the old <code>-application:willFinishLaunchingWithOptions:</code> code is that we now need to tell the <code>UIWindow</code> which scene it is part of. This can be done using the new <code>UIWindow(windowScene: windowScene)</code> init method, but windows can also be dynamically moved between scenes so there is also a <code>windowScene</code> property on <code>UIWindow</code> instances which can be changed at any time.</p>
Automatically Correct Insta360 Studio Snapshot Dates
https://ikyle.me/blog/2020/correct-insta360-one-x-snapshot-date
2020-02-14T21:23:47.172803+00:00
2020-02-14T21:23:47.172803+00:00
A simple script to fix the creation date of exported snapshots from Insta360 One X Studio
<p><img src="https://ikyle.me/blog/2020/correct-insta360-one-x-snapshot-date/insta360-studio-1.jpg" alt="Insta360 Studio 2019 for the Insta360 One X" /></p>
<p>When you import a 360º photo into the desktop Insta360 Studio software, one of the export options is to take a "normal" photo (called "FreeCapture" in Insta360's software, or "Overcapture" in the GoPro Fusion software) from the original 360º media, saving this as a snapshot.</p>
<p>Unfortunately these snapshots are just that, exported snapshots of the preview area with no camera metadata.<br />
Lack of metadata is especially annoying when sorting by date (as all phones do), as re-framed photos will not appear next to the 360º photos themselves, but next will appear as being from the day you processed it.</p>
<p>Fortunately, the name includes all the date and time information needed.</p>
<p><img src="https://ikyle.me/blog/2020/correct-insta360-one-x-snapshot-date/before-filename.png" alt="Image file save name suggestion IMG_20191230_120453_00_105_2020-02-14_17-17-37_screenshot.jpg" /></p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>IMG_20191230_120453_00_105_2020-02-14_17-17-37_screenshot.jpg
Pattern
IMG_|yr|mon|day|_|hr|min|sec|_00_#_[date screenshot saved]_screenshot.jpg
</code></pre></div>
<p>This means it is easy to create a simple script to fix the files after the fact, allowing you to use Insta360 Studio to snapshot lots of different angles of photos and video and then run a script to clean things up afterwards (instead of manually fixing each image file as you go).</p>
<h1>Date Correction Script</h1>
<p>The script needs to follow these simple instructions:</p>
<ol>
<li>Find all jpg's with <code>screenshot</code> in the name.</li>
<li>Extract the date from the file name.</li>
<li>Modify the exif date taken data to match.</li>
<li>Save the new metadata.</li>
</ol>
<p>(Though there are other steps, like merging in some metadata from the original 360 file, which could be taken to improve it.)</p>
<h2>The script</h2>
<p>The only external library used is one to extract the metadata and modify it, <a href="https://github.com/hMatoba/Piexif">Piexif</a>.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">import</span> <span style="color: #000000">os</span>
<span style="color: #A90D91">import</span> <span style="color: #000000">re</span>
<span style="color: #A90D91">import</span> <span style="color: #000000">datetime</span>
<span style="color: #A90D91">import</span> <span style="color: #000000">piexif</span>
<span style="color: #000000">filepath</span> <span style="color: #000000">=</span> <span style="color: #000000">os.path.abspath</span>(<span style="color: #C41A16">"."</span>)
<span style="color: #177500"># Find files with `screenshot` in the name.</span>
<span style="color: #000000">files</span> <span style="color: #000000">=</span> [<span style="color: #000000">f</span> <span style="color: #A90D91">for</span> <span style="color: #000000">f</span> <span style="color: #000000">in</span> <span style="color: #000000">os.listdir</span>(<span style="color: #000000">filepath</span>) <span style="color: #A90D91">if</span> <span style="color: #C41A16">"screenshot"</span> <span style="color: #000000">in</span> <span style="color: #000000">f</span> <span style="color: #000000">and</span> <span style="color: #000000">f.endswith</span>(<span style="color: #C41A16">".jpg"</span>)]
<span style="color: #A90D91">print</span>(<span style="color: #000000">files</span>)
<span style="color: #A90D91">for</span> <span style="color: #000000">filename</span> <span style="color: #000000">in</span> <span style="color: #000000">files</span>:
<span style="color: #A90D91">print</span>(<span style="color: #000000">filename</span>)
<span style="color: #177500"># Extract datetime</span>
<span style="color: #000000">date_rex</span> <span style="color: #000000">=</span> <span style="color: #C41A16">r"(20\d\d)(\d\d)(\d\d)_(\d\d)(\d\d)(\d\d)"</span>
<span style="color: #000000">date_ranges</span> <span style="color: #000000">=</span> <span style="color: #000000">re.search</span>(<span style="color: #000000">date_rex</span>, <span style="color: #000000">filename</span>)
<span style="color: #177500"># Date</span>
<span style="color: #000000">year</span> <span style="color: #000000">=</span> <span style="color: #000000">date_ranges.group</span>(<span style="color: #1C01CE">1</span>)
<span style="color: #000000">month</span> <span style="color: #000000">=</span> <span style="color: #000000">date_ranges.group</span>(<span style="color: #1C01CE">2</span>)
<span style="color: #000000">day</span> <span style="color: #000000">=</span> <span style="color: #000000">date_ranges.group</span>(<span style="color: #1C01CE">3</span>)
<span style="color: #177500"># Time</span>
<span style="color: #000000">hour</span> <span style="color: #000000">=</span> <span style="color: #000000">date_ranges.group</span>(<span style="color: #1C01CE">4</span>)
<span style="color: #000000">minute</span> <span style="color: #000000">=</span> <span style="color: #000000">date_ranges.group</span>(<span style="color: #1C01CE">5</span>)
<span style="color: #000000">second</span> <span style="color: #000000">=</span> <span style="color: #000000">date_ranges.group</span>(<span style="color: #1C01CE">6</span>)
<span style="color: #177500"># date text</span>
<span style="color: #000000">dt_text</span> <span style="color: #000000">=</span> <span style="color: #C41A16">f"{year}:{month}:{day} {hour}:{month}:{second}"</span>
<span style="color: #A90D91">print</span>(<span style="color: #000000">dt_text</span>)
<span style="color: #177500"># Image file path</span>
<span style="color: #000000">image_filepath</span> <span style="color: #000000">=</span> <span style="color: #000000">os.path.join</span>(<span style="color: #000000">filepath</span>, <span style="color: #000000">filename</span>)
<span style="color: #A90D91">print</span>(<span style="color: #000000">image_filepath</span>)
<span style="color: #000000">exif_dict</span> <span style="color: #000000">=</span> <span style="color: #000000">piexif.load</span>(<span style="color: #000000">image_filepath</span>)
<span style="color: #A90D91">print</span>(<span style="color: #000000">exif_dict</span>) <span style="color: #177500"># Before</span>
<span style="color: #000000">exif_dict</span>[<span style="color: #C41A16">"0th"</span>][<span style="color: #000000">piexif.ImageIFD.DateTime</span>] <span style="color: #000000">=</span> <span style="color: #000000">dt_text</span>
<span style="color: #000000">exif_dict</span>[<span style="color: #C41A16">"Exif"</span>][<span style="color: #000000">piexif.ExifIFD.DateTimeOriginal</span>] <span style="color: #000000">=</span> <span style="color: #000000">dt_text</span>
<span style="color: #A90D91">print</span>(<span style="color: #000000">exif_dict</span>) <span style="color: #177500"># After</span>
<span style="color: #177500"># Save new metadata</span>
<span style="color: #000000">exif_bytes</span> <span style="color: #000000">=</span> <span style="color: #000000">piexif.dump</span>(<span style="color: #000000">exif_dict</span>)
<span style="color: #000000">piexif.insert</span>(<span style="color: #000000">exif_bytes</span>, <span style="color: #000000">image_filepath</span>)
</code></pre></div>
<p>This python script should be safe to re-run multiple times on the same directory (although it could cut down on fileIO by comparing the existing date metadata to see if it is correct before overriding it).</p>
<p>Example output from <code>piexif.load(img)</code> before and after:</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">Before</span>:
<span style="color: #000000">{</span><span style="color: #C41A16">'0th'</span>: <span style="color: #000000">{}</span>, <span style="color: #C41A16">'Exif'</span>: <span style="color: #000000">{}</span>, <span style="color: #C41A16">'GPS'</span>: <span style="color: #000000">{}</span>, <span style="color: #C41A16">'Interop'</span>: <span style="color: #000000">{}</span>, <span style="color: #C41A16">'1st'</span>: <span style="color: #000000">{}</span>, <span style="color: #C41A16">'thumbnail'</span>: <span style="color: #A90D91">None</span><span style="color: #000000">}</span>
<span style="color: #A90D91">After</span>:
<span style="color: #000000">{</span><span style="color: #C41A16">'0th'</span>: <span style="color: #000000">{</span><span style="color: #1C01CE">306</span>: <span style="color: #C41A16">'2019:12:30 12:04:53'</span><span style="color: #000000">}</span>, <span style="color: #C41A16">'Exif'</span>: <span style="color: #000000">{</span><span style="color: #1C01CE">36867</span>: <span style="color: #C41A16">'2019:12:30 12:04:53'</span><span style="color: #000000">}</span>, <span style="color: #C41A16">'GPS'</span>: <span style="color: #000000">{}</span>, <span style="color: #C41A16">'Interop'</span>: <span style="color: #000000">{}</span>, <span style="color: #C41A16">'1st'</span>: <span style="color: #000000">{}</span>, <span style="color: #C41A16">'thumbnail'</span>: <span style="color: #A90D91">None</span><span style="color: #000000">}</span>
</code></pre></div>
<p>The script is also saved as a <a href="https://gist.github.com/kylehowells/5ef3803d7acd3775ba611a6c5a6cee83">gist on Github</a>.</p>
My New Website
https://ikyle.me/blog/2017/my-new-website
2017-01-15T02:36:14.846980+00:00
2017-01-14T21:48:16.608110+00:00
The design and architecture of my new website
<p><img src="https://ikyle.me/blog/2017/my-new-website/homepage.png" alt="" /></p>
<p>Since I started learning programming I've had a few different websites. I've used Weebly, Wordpress and Tumblr. But I'd always wanted to (and had never gotten around to) make my own website.<br />
Now, finally, I have.</p>
<p><strong>TLDR:</strong> The website is written in Python using <a href="http://flask.pocoo.org">Flask</a>, running on an Ubuntu VPS from <a href="https://m.do.co/c/44bf93142995">DigitalOcean</a> and the blog posts are written in <a href="http://commonmark.org">CommonMark</a> (for now).<br />
The front end is hand written HTML and CSS with minimal Javascript (2 main functions).</p>
<h2>Visual Design</h2>
<p>Before I started on writing any server side code, I created a few example pages to establish the visual style I wanted.</p>
<p>I am not a designer. I am a programmer who likes good UI and wishes he was good at design.</p>
<p>So to compensate, I look around for examples of design I like and save them. I started doing this with designs I particularly like on dribble and other sites.<br />
At the time of writing this, my "UI" folder on my NAS contains 17 sub folders and 1089 images (yes, I should probably just use pinterest. I don't because I want them locally so I can run image/color analysis algorithms on them, if I ever get around to writing any that is...).</p>
<h3>UI Prototypes</h3>
<p>I'm never sure how to start things. Whether it is a piece of writing, UI design or a website. I struggle to think of how to introduce things. One option I could have used was to simply have my website start on the blog index. However, I wanted multiple different sections to the website. Just as importantly I wanted them to feel like different sections, not simply set apart pages of the blog. The home page was inspired by <a href="http://chpwn.com">@chpwn</a> and a few other developers (though the rest have all changed their websites' designs).</p>
<p><img src="https://ikyle.me/blog/2017/my-new-website/chpwn.png" alt="" /></p>
<p>For the rest of the site I used the top navigation bar (found on almost every website) and played around until I was happy with the design. Though I ended up tweaking things here and there anyway when I came to implement it properly.</p>
<p><img src="https://ikyle.me/blog/2017/my-new-website/example_design.jpg" alt="" /></p>
<p>If you are wondering about the example blog post, the title is obviously nonsense but, rather than use Lorem Ipsum for the body text, I used <a href="https://www.pacifict.com/Story/">this incredible story</a> about the Graphing Calculator Apple bundled with the original PowerPC computers.</p>
<h3>The Final Designs</h3>
<p>I added a drop shadow to the navigation bar, vertically centred the home page and went back on my decision to let the content pages (such as the portfolio project pages and blog posts) go full width no matter the screen width. Overall though I kept things mostly the same.</p>
<p>For the projects themselves I didn't actually decide on the design until I started writing the actual site. I originally planned for each of the projects to get a custom design specific to that project. That plan ended when I realised I don't have enough imagination to pull off more than 10 custom designs, each for incredibly similar projects.</p>
<p><img src="https://ikyle.me/blog/2017/my-new-website/project_design.jpg" alt="" /></p>
<p>In the end I created a template I can customise to different projects and alter slightly for different types of projects. The app pages get the app icon and a link to the AppStore. The other types of projects simply get links to whatever official website the project has.</p>
<p><img src="https://ikyle.me/blog/2017/my-new-website/project_design2.jpg" alt="" /></p>
<p>I spent a lot of time finding personal websites I liked the designs of and then looking at, and comparing, the “about me” pages. In the end what I ended up writing turned into a mini-portfolio page on its own. I’m not completely happy with the design but I’ll keep working on it over time.</p>
<p><img src="https://ikyle.me/blog/2017/my-new-website/about_me.jpg" alt="" /></p>
<p>I had originally planned to have the blog posts go full width across the browser window. However, I made this decision while using a 13" MacBook Pro as my main computer. In the interim, between deciding that and implementing it, I switched to using a desktop computer with extra wide screen monitor and reconsidered my earlier position.<br />
For overall style I tried to emulate the look of Github’s README files.</p>
<p>A feature I particularly like on Medium, that lots of writing applications have (but very few websites), is the estimated reading time they display right at the top of the page. As soon as I load the article I know roughly how long I need, and if I should set it aside to read later.<br />
If it is within the last week, the published, and <em>modified</em> (I believe both should be shown on blog posts) labels display how long ago that event was, instead of the actual date. However, the actual date and time is always included in the ‘title’ attribute. Hovering over the relative timestamps should prompt the browser to display the real time stamp.</p>
<p><img src="https://ikyle.me/blog/2017/my-new-website/blog_post.jpg" alt="" /></p>
<p>For categorising posts I decided to use <em>tags</em> instead of categories. In my mind categories are like folders. A post can be put inside a single category. Whereas, multiple tags can be attached to a single article. So if I write about releasing a new iOS fitness app I can tag it with: iOS, fitness, press release, etc...<br />
Later I can lookup all the iOS related blog posts I’ve written, or every press release, etc...</p>
<h2>Code Design and Implementation</h2>
<h3>Project Structure</h3>
<p>The public facing web server itself is Nginx, running on an Ubuntu VPS from <a href="https://m.do.co/c/44bf93142995">DigitalOcean</a>. Nginx is set up to try to serve files in the following order:</p>
<ol>
<li>The static resources directory.</li>
<li>The static pages directory.</li>
<li>The cache directory.</li>
<li>Finally it will forward the request to the python web app.</li>
</ol>
<p>This setup minimises the requests that make it to the python application and allows me to easily cache blog posts, and any other pages I want to be dynamic some of the time.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">server</span> {
<span style="color: #A90D91">listen</span> <span style="color: #1C01CE">80</span>;
<span style="color: #A90D91">server_name</span> <span style="color: #C41A16">ikyle.me</span> <span style="color: #C41A16">www.ikyle.me</span>;
<span style="color: #A90D91">charset</span> <span style="color: #C41A16">utf-8</span>;
<span style="color: #A90D91">root</span> <span style="color: #C41A16">/var/www/ikyle</span>;
<span style="color: #A90D91">index</span> <span style="color: #C41A16">index.html</span> <span style="color: #C41A16">index.htm</span>;
<span style="color: #A90D91">location</span> <span style="color: #C41A16">/</span> {
<span style="color: #A90D91">try_files</span> <span style="color: #C41A16">/static_files</span><span style="color: #000000">$uri</span> <span style="color: #C41A16">/static_files</span><span style="color: #000000">$uri/index.html</span>
<span style="color: #C41A16">/static_pages</span><span style="color: #000000">$uri</span> <span style="color: #C41A16">/static_pages</span><span style="color: #000000">$uri/index.html</span>
<span style="color: #C41A16">/cache</span><span style="color: #000000">$uri</span> <span style="color: #C41A16">/cache</span><span style="color: #000000">$uri/index.html</span> <span style="color: #C41A16">@blog</span>;
}
<span style="color: #A90D91">location</span> <span style="color: #C41A16">@blog</span> {
<span style="color: #A90D91">include</span> <span style="color: #C41A16">uwsgi_params</span>;
<span style="color: #A90D91">uwsgi_pass</span> <span style="color: #C41A16">unix:/var/www/ikyle/ikyle_uwsgi.sock</span>;
}
}
</code></pre></div>
<p>One optimisation I have been thinking about doing is to: move the cache in front of the static files directory; extend the python application to monitor for changes to css and javascript files inside the static directory; and have it automatically run a minifier on those files, placing the output in the cache directory.</p>
<p>I don’t know what the conventions are for structuring Python applications, but this is the file structure I went with.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>Root
+-- error_pages/
| Static error pages (404, etc...) are stored in here
+-- static_files/
| Static files (css, js, images, etc...) go in here (can be cached)
+-- static_pages/
| Static pages from the website (such as the about me and portfolio pages) go in here. (not to be cached)
|
+-- cache/
| Rendered blog posts, etc go in here.
|
|
+-- portfolio/
| +-- input/
| | +-- categories.json
| | | Defines the different categories of portfolio projects, and their order
| | *-- 'folder_name'/
| | Projects each have their own folder
| | +-- media/
| | | This folder contains all the images and videos for the project
| | +-- info.json
| | | This file defines everything about the project
| | *-- 'file'.md
| | Any markdown files will be rendering into HTML files and placed as directories to the project itself
| |
| +-- templates/
| | The templates for the porfolio pages go in here
| |
| `-- renderer.py
| Processes the input files and generates the resulting HTML pages
|
|
+-- blog.db
| The SQLite file that acts as the websites database.
+-- app.py
| Application entry point
+-- api/
| +-- api.py
| | The python api through which the websites posts are accessed and searched
| `-- utility.py
| Functions shared across different pages, such as rendering Markdown and caching pages.
`-- web/
+-- templates/
| The html templates that generate the web pages.
| +-- blog/
| The folder the blog templates are stored
| `-- admin/
| The folder the admin templates are stored
|
+-- webapi.py
| The public facing json web api endpoint.
`-- blog.py
The public html blog
</code></pre></div>
<p>The top few directories are simply to place HTML, CSS and other static files in. The next part is the portfolio directory. This acts as its own subproject, separate from the rest of the application.</p>
<p>As most of my projects are very similar (predominantly being iOS apps) I decided that I would create a template, like I had set up for the blog posts.<br />
To create a new project page I create a new directory for it. Inside that directory I create an <code>info.json</code> file and fill out the relevant information, such as: category; title; description; description; any press or reviews it has received; relevant images and videos; and links to related web pages.<br />
The <code>rendered.py</code> script takes the information from the <code>info.json</code> files, passes it into the template and saves the result, along with the media, into the <code>static_pages</code> directory.</p>
<p>At the moment, I just run this locally on my computer and sync the resulting files onto the server. Adding a new project to my portfolio isn’t something I do regularly enough for me to have created a web UI for this yet.</p>
<p>Finally, the last chunk is the actual Flask application itself.</p>
<h3>The Flask Application</h3>
<p>The application is separated into 3 sections: the <code>api</code>; the <code>blog</code>; and the <code>webapi</code>.</p>
<p>The <code>api</code> directory contains a utilities file, with functions I use throughout the application, and the <code>api.py</code> file that handles all database access for blog posts.</p>
<p>As you might have noticed in the file directory structure, I use SQLite for the database. Before you start, yes I know.</p>
<blockquote>
<p><a href="https://www.sqlite.org/about.html">Think of SQLite not as a replacement for Oracle but as a replacement for fopen()</a></p>
</blockquote>
<p>However, the reasons I choose to use SQLite are as follows.</p>
<ul>
<li>My dataset is small.<br />
The dataset is my blog posts. Even after years of active blogging, we are only talking a few thousand text documents.</li>
<li>My expected load is minimal.<br />
As much as I’d like to imagine otherwise, my posts are unlikely to generate very much traffic.</li>
<li>I’m lazy.<br />
Choosing SQLite gave me: a quick and easy setup on the web server; the ability to run the website locally on OS X with minimal fuss; trivial backups; and <a href="https://www.sqlite.org/fts3.html">easy full text search</a>.</li>
</ul>
<p>Having said that, I am reconsidering the way I handle storing blog posts, which I’ll detail more in the possible changes section at the end of this post.</p>
<p>The <code>api.py</code> file handles opening, closing and querying the database. Posts are returned as a custom <code>Dict</code> subclass that allows you to access keys like attributes.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500"># Convience Dict Subclass</span>
<span style="color: #177500"># Allows dict["name"] to be queried via dict.name</span>
<span style="color: #A90D91">class</span> <span style="color: #3F6E75">DBItem</span>(<span style="color: #A90D91">dict</span>):
<span style="color: #A90D91">def</span> <span style="color: #000000">__getattr__</span>(<span style="color: #5B269A">self</span>, <span style="color: #000000">name</span>):
<span style="color: #A90D91">if</span> <span style="color: #000000">name</span> <span style="color: #000000">in</span> <span style="color: #5B269A">self</span>:
<span style="color: #A90D91">return</span> <span style="color: #5B269A">self</span>[<span style="color: #000000">name</span>]
<span style="color: #A90D91">else</span>:
<span style="color: #A90D91">return</span> <span style="color: #A90D91">None</span>
<span style="color: #A90D91">def</span> <span style="color: #000000">__setattr__</span>(<span style="color: #5B269A">self</span>, <span style="color: #000000">name</span>, <span style="color: #000000">value</span>):
<span style="color: #5B269A">self</span>[<span style="color: #000000">name</span>] <span style="color: #000000">=</span> <span style="color: #000000">value</span>
<span style="color: #A90D91">def</span> <span style="color: #000000">__delattr__</span>(<span style="color: #5B269A">self</span>, <span style="color: #000000">name</span>):
<span style="color: #A90D91">if</span> <span style="color: #000000">name</span> <span style="color: #000000">in</span> <span style="color: #5B269A">self</span>:
<span style="color: #A90D91">del</span> <span style="color: #5B269A">self</span>[<span style="color: #000000">name</span>]
</code></pre></div>
<p>The biggest part of the file is the <code>get_posts()</code> function. Rather than using several different functions to query different things, this takes several parameters, validates they are within allowed ranges, and builds the correct query string. Then it processes the result into an array of post objects.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #177500"># Search posts</span>
<span style="color: #A90D91">def</span> <span style="color: #000000">get_posts</span>(<span style="color: #000000">query=</span><span style="color: #A90D91">None</span>, <span style="color: #000000">sortDescending=</span><span style="color: #A90D91">True</span>, <span style="color: #000000">before=</span><span style="color: #A90D91">None</span>, <span style="color: #000000">after=</span><span style="color: #A90D91">None</span>, <span style="color: #000000">summary=</span><span style="color: #A90D91">True</span>, <span style="color: #000000">limit=</span><span style="color: #1C01CE">25</span>, <span style="color: #000000">tags=</span><span style="color: #A90D91">None</span>):
<span style="color: #000000">...</span>
<span style="color: #177500"># How many results does the search potentially return</span>
<span style="color: #A90D91">def</span> <span style="color: #000000">get_posts_count</span>(<span style="color: #000000">query=</span><span style="color: #A90D91">None</span>, <span style="color: #000000">before=</span><span style="color: #A90D91">None</span>, <span style="color: #000000">after=</span><span style="color: #A90D91">None</span>, <span style="color: #000000">tags=</span><span style="color: #A90D91">None</span>):
<span style="color: #000000">...</span>
</code></pre></div>
<p>This function is delicate enough that, if this was for something other than my personal blog, I would separate it into 3 distinct parts (the validation; query string building and result building) which could have tests run against them. However, setting up and writing tests for this, (and the different URL endpoints) isn’t worth it to me. If there is a bug I know exactly which file handles that part of the website and can deploy a fix within minutes.</p>
<p>The 2 web facing parts of the website are split between <code>webapi.py</code> and <code>blog.py</code>, which are each separate <a href="http://flask.pocoo.org/docs/0.12/blueprints/">Flask Blueprints</a>.</p>
<p><code>app.py</code> sets up Flask and adds my custom functions to the <a href="http://jinja.pocoo.org/docs/2.9/">jinja2</a> template engine and registers the <code>webapi</code> and <code>blog</code> blueprints to the relevant <code>url_prefix</code>.</p>
<p><code>webapi.py</code> is basically a thin JSON wrapper around the blog api.</p>
<p>For example: <code>ikyle.me/blog/api/post?id=1</code> will return the first blog post’s <code>Dict</code> (which should be this post) dumped as a JSON file (including the content as markdown, rather than HTML).<br />
This is the relevant code (just with some sanity checks removed).</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #000000">post</span> <span style="color: #000000">=</span> <span style="color: #000000">api.get_post</span>( <span style="color: #000000">request.args.get</span>(<span style="color: #C41A16">'id'</span>) )
<span style="color: #000000">r</span> <span style="color: #000000">=</span> <span style="color: #000000">flask.make_response</span>( <span style="color: #000000">json.dumps</span>(<span style="color: #000000">post</span>) );
<span style="color: #000000">r.mimetype</span> <span style="color: #000000">=</span> <span style="color: #C41A16">'application/json'</span>;
<span style="color: #A90D91">return</span> <span style="color: #000000">r</span>
</code></pre></div>
<p>I created the JSON api so that I have easy API access to my blog, in case I want to make an iOS app or something similar. However, the only endpoints that require authentication are the ones to create, modify or delete a blog post. The two needed for browsing are open.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>/blog/api/post?id=
/blog/api/search
</code></pre></div>
<p><code>blog.py</code> is similar to <code>webapi.py</code> but with the extras required for managing a website rather than an API endpoint. It queries the api, renders the posts, handles (most) server errors and generates the blog's <a href="https://ikyle.me/blog/recent.xml">Atom feed</a> (yes it has one, I just haven’t added an icon linking to it yet).</p>
<p>The Atom feed is generated by the <a href="http://flask.pocoo.org/snippets/10/">AtomFeed class from the Werkzeug contrib package</a>.</p>
<p>I also use <a href="https://flask-login.readthedocs.io/en/latest/">Flask-Login</a> to manage access to a set of very basic admin features I built into the website. These consist of a basic blog post editor and buttons that appear allowing me to create, edit or delete posts.</p>
<p><img src="https://ikyle.me/blog/2017/my-new-website/blog_editor.jpg" alt="" /></p>
<p>As I (for the moment) use unmodified <a href="http://commonmark.org">CommonMark</a>, I am able to use the <a href="https://github.com/jgm/commonmark.js">JavaScript reference implementation</a> to locally generate a live preview of the post as I am writing it. This is almost identical to the <a href="http://spec.commonmark.org/dingus/">commonmark.js dingus</a>. One of the extra things I need to do is refresh the syntax highlighting.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code><span style="color: #A90D91">var</span> <span style="color: #000000">commonmark</span> <span style="color: #000000">=</span> <span style="color: #A90D91">window</span>.<span style="color: #000000">commonmark</span>;
<span style="color: #A90D91">var</span> <span style="color: #000000">writer</span> <span style="color: #000000">=</span> <span style="color: #A90D91">new</span> <span style="color: #000000">commonmark</span>.<span style="color: #000000">HtmlRenderer</span>();
<span style="color: #A90D91">var</span> <span style="color: #000000">htmlwriter</span> <span style="color: #000000">=</span> <span style="color: #A90D91">new</span> <span style="color: #000000">commonmark</span>.<span style="color: #000000">HtmlRenderer</span>();
<span style="color: #A90D91">var</span> <span style="color: #000000">xmlwriter</span> <span style="color: #000000">=</span> <span style="color: #A90D91">new</span> <span style="color: #000000">commonmark</span>.<span style="color: #000000">XmlRenderer</span>();
<span style="color: #A90D91">var</span> <span style="color: #000000">reader</span> <span style="color: #000000">=</span> <span style="color: #A90D91">new</span> <span style="color: #000000">commonmark</span>.<span style="color: #000000">Parser</span>();
<span style="color: #A90D91">function</span> <span style="color: #000000">debounce</span>(<span style="color: #000000">func</span>, <span style="color: #000000">wait</span>, <span style="color: #000000">immediate</span>){ <span style="color: #177500">/* https://davidwalsh.name/javascript-debounce-function */</span>}
<span style="color: #A90D91">document</span>.<span style="color: #000000">addEventListener</span>(<span style="color: #C41A16">"DOMContentLoaded"</span>, <span style="color: #A90D91">function</span>(<span style="color: #000000">event</span>) {
<span style="color: #000000">hljs</span>.<span style="color: #000000">configure</span>({<span style="color: #000000">languages:</span> []}); <span style="color: #177500">// Don't auto detect languages</span>
<span style="color: #A90D91">var</span> <span style="color: #000000">article_text</span> <span style="color: #000000">=</span> <span style="color: #A90D91">document</span>.<span style="color: #000000">getElementById</span>(<span style="color: #C41A16">"article_textarea"</span>);
<span style="color: #A90D91">var</span> <span style="color: #000000">preview_holder</span> <span style="color: #000000">=</span> <span style="color: #A90D91">document</span>.<span style="color: #000000">getElementById</span>(<span style="color: #C41A16">"article_preview_div"</span>);
<span style="color: #A90D91">var</span> <span style="color: #000000">render_markdown</span> <span style="color: #000000">=</span> <span style="color: #000000">debounce</span>(<span style="color: #A90D91">function</span>() {
<span style="color: #A90D91">var</span> <span style="color: #000000">parsed</span> <span style="color: #000000">=</span> <span style="color: #000000">reader</span>.<span style="color: #000000">parse</span>(<span style="color: #000000">article_text</span>.<span style="color: #000000">value</span>);
<span style="color: #A90D91">var</span> <span style="color: #000000">renderedHTML</span> <span style="color: #000000">=</span> <span style="color: #000000">writer</span>.<span style="color: #000000">render</span>(<span style="color: #000000">parsed</span>);
<span style="color: #000000">preview_holder</span>.<span style="color: #000000">innerHTML</span> <span style="color: #000000">=</span> <span style="color: #000000">renderedHTML</span>;
<span style="color: #000000">highlight_code</span>(<span style="color: #000000">preview_holder</span>);
}, <span style="color: #1C01CE">250</span>);
<span style="color: #000000">article_text</span>.<span style="color: #000000">onchange</span> <span style="color: #000000">=</span> <span style="color: #A90D91">function</span>() { <span style="color: #000000">render_markdown</span>(); };
<span style="color: #000000">article_text</span>.<span style="color: #000000">oninput</span> <span style="color: #000000">=</span> <span style="color: #A90D91">function</span>() { <span style="color: #000000">render_markdown</span>(); };
<span style="color: #177500">// Call them once at the start</span>
<span style="color: #000000">render_markdown</span>();
}
<span style="color: #A90D91">function</span> <span style="color: #000000">highlight_code</span>(<span style="color: #000000">element</span>) {
<span style="color: #A90D91">var</span> <span style="color: #000000">elements</span> <span style="color: #000000">=</span> <span style="color: #000000">element</span>.<span style="color: #000000">getElementsByTagName</span>(<span style="color: #C41A16">'pre'</span>);
<span style="color: #A90D91">for</span> (<span style="color: #A90D91">var</span> <span style="color: #000000">i</span> <span style="color: #000000">=</span> <span style="color: #1C01CE">0</span>; <span style="color: #000000">i</span> <span style="color: #000000"><</span> <span style="color: #000000">elements</span>.<span style="color: #000000">length</span>; <span style="color: #000000">i++</span>) {
<span style="color: #A90D91">var</span> <span style="color: #000000">pre_block</span> <span style="color: #000000">=</span> <span style="color: #000000">elements</span>[<span style="color: #000000">i</span>];
<span style="color: #A90D91">var</span> <span style="color: #000000">possible_block</span> <span style="color: #000000">=</span> <span style="color: #000000">pre_block</span>.<span style="color: #000000">children</span>[<span style="color: #1C01CE">0</span>];
<span style="color: #A90D91">if</span> (<span style="color: #000000">possible_block</span>.<span style="color: #000000">tagName</span> <span style="color: #000000">==</span> <span style="color: #C41A16">"CODE"</span>) {
<span style="color: #000000">hljs</span>.<span style="color: #000000">highlightBlock</span>(<span style="color: #000000">possible_block</span>);
}
}
}
</code></pre></div>
<h2>Future Changes</h2>
<p>I’m happy I have the site at a point I can push it live. However, this is not how the website will always be. Even now, there are lots of things I want to change.</p>
<p>At the moment, the website isn’t optimised very much. I designed <strong>a cache</strong> directory into the design from the beginning, but don’t use it at present.</p>
<p>Both the <em>blog posts</em> and <em>atom feed</em> are dynamically generated on every request. An obvious gain would be to cache both of these.</p>
<p>The atom feed could be saved and only regenerated every time a blog post is changed, rather than on every request.</p>
<p>Blog posts have friendly dynamic timestamps, but only for the first week (which is also the time a post is most likely to be edited). Thereafter, they could be cached indefinitely, only needing to be regenerated if modified. Alternatively, the friendly timestamps could be generated locally with javascript, allowing the posts to be cached from the start.</p>
<p>I am currently writing this post using <a href="https://ia.net/writer/">iA Writer</a> on my MacBook, rather than in the post editor I spent the time to make, because I didn’t think to add the ability to save (or even the concept of) <strong>drafts</strong>.</p>
<p>Using CommonMark has its advantages. I can use <a href="https://github.com/jgm/CommonMark/wiki/List-of-CommonMark-Implementations">any CommonMark compatible renderer</a> unmodified and my blog posts will render correctly.<br />
Despite this, I don’t think my blog will use standard CommonMark for very long.</p>
<p>One missing feature I was shocked to find out isn’t part of either Markdown or CommonMark is footnotes. I was particularly surprised given that John Gruber is the <a href="https://daringfireball.net/projects/markdown/">original author of Markdown</a> and his website, <a href="https://daringfireball.net">daringfireball.net</a>, uses footnotes in almost every post.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>This should be a footnote.[^comment]
[<span style="color: #000000">^comment</span>]: <span style="color: #836C28">Sadly this syntax isn't supported for some reason.</span>
</code></pre></div>
<p>I would have used this feature extensively while writing this article. Almost everywhere I have included a comment in (brackets), and some of the comments blocked by commas, would have instead been a footnote. I left out a lot of small related comments I have about different sections of this post because I would have put them in a footnote, and adding them into the main body of the text would divert that section off course too far from its original topic.</p>
<p>If I’m going to <strong>give up compatibility with the CommonMark spec</strong> I might as well go all the way. Other non-standard extensions I’d like include:</p>
<ul>
<li><a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#tables">Tables</a></li>
<li><a href="https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments">Checkboxes</a></li>
<li><a href="https://github.com/cmrd-senya/markdown-it-html5-embed">The image syntax extended to include videos automatically</a>, based on the file extension.</li>
</ul>
<p>John Gruber has stated that for things Markdown doesn’t include you should use HTML directly. However, one of my reasons for using Markdown is to avoid using HTML directly.<br />
Keep in mind that making a native iOS app is always faintly at the back of my mind, even if I end up never getting around to it. Including HTML in your markdown might be fine if the markdown is just going to be rendered out to HTML. But, if the result is going to be rendered differently (by iOS’s text rendering engine for example) HMTL is just going to get in the way and cause you issues.</p>
<p>Talking about custom changes to Markdown leads me neatly into the next point. I will, probably, completely change the way blog posts are handled.</p>
<p>At the moment, blog posts are stored in the SQLite database, and blog post’s images are handled... well that’s another feature, like drafts, I didn’t realise I needed to handle until I started writing this blog post. I manually rsync’d the images for <em>this post</em> onto the server.</p>
<p>An article I read a little while ago which really caught my imagination, and that I haven’t seen mentioned much online, was this blog post announcing <a href="https://ia.net/writer/blog/ia-writer-4/">iA Writer 4</a>.</p>
<p>In it they mentioned a quote from John Gruber about Markdown’s image syntax (which I keep forgetting).</p>
<blockquote>
<p>My best idea for good Markdown img syntax would be to just paste in a URL ending in .jpg/.png/.gif etc.</p>
</blockquote>
<p>I’ve already mentioned I think the image syntax should be expanded to include .mp4, .mp3, etc... However, iA Labs took the idea a step further. This allows tables, source code and even other bits of text to be separated into external csv, source and text files.</p>
<p><img src="https://ikyle.me/blog/2017/my-new-website/iA_Writer_4.jpg" alt="" /></p>
<p>They even have an example spec for <a href="https://github.com/iainc/Markdown-Content-Blocks"><strong>file transclusion</strong></a>, as the concept is named.</p>
<p>My revised <strong>handling of blog posts</strong> would look much more like iA Writer’s handling of its library, or even how I handle the portfolio, than it looks at the moment.</p>
<div class="highlight" style="background: #ffffff"><pre style="line-height: 125%"><span></span><code>Root
+-- blog/
| +-- posts/
| | *-- "year"/
| | | Each year with its own directory.
| | | *-- "post_title"/
| | | | Each post has its own directory
| | | +-- info.json
| | | | This file defines the title, description, author, tags, and publish/modification date
| | | +-- post.md
| | | | This would be the custom CommonMark for the post itself.
| | | `-- resources/
| | | | This directory would be contain the images, text files and other files referenced in the post.md file.
</code></pre></div>
<p>I considered including the metadata at the top of the post.md file, like <a href="https://github.com/NSHipster/articles">some websites do</a>, but I prefer a slightly more structured layout.</p>
<p>Ideally, the python application would monitor the <code>posts</code> directory for changes and delete the cached HTML for the relevant blog post when needed. Then, after a short delay it’d re-render the post’s cached HTML file.</p>
<p>I would still use the SQLite database. However, instead of being the primary location for posts to be stored it would mirror the tags, metadata and the full text of blog posts. Rather than loading in the markdown directly, I’d parse the markdown and extract just the text (in much the same way I currently generate the post’s word count) loading that into the database, to make use of the SQLite full text search.</p>
<p>Other items I have notes to add in the future include:</p>
<ul>
<li>Image uploading from my post editor</li>
<li>A better admin page</li>
<li>A web based portfolio project creation page/editor</li>
</ul>
<h2>Summary</h2>
<p>I’m never completely satisfied with anything I make, there’s always another feature I could add, a better way to do something, a more efficient way to achieve what I already have.</p>
<p>However, I am proud of this website.</p>
<p>I have done other websites before, but they all used existing templates, a ready made CMS or were just a single static HTML page. This was my first real server side project, and one of my first attempts at doing the design for a larger project myself.</p>