<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Artem Novichkov</title><generator>teletype.in</generator><description><![CDATA[Bearded iOS developer 👨🏻‍💻]]></description><image><url>https://teletype.in/files/03/d6/03d66c0a-df20-4426-b662-963022f85507.jpeg</url><title>Artem Novichkov</title><link>https://blog.artemnovichkov.com/</link></image><link>https://blog.artemnovichkov.com/?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/artemnovichkov?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/artemnovichkov?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Fri, 10 Apr 2026 22:23:21 GMT</pubDate><lastBuildDate>Fri, 10 Apr 2026 22:23:21 GMT</lastBuildDate><item><guid isPermaLink="true">https://blog.artemnovichkov.com/keyboard-layout-guide</guid><link>https://blog.artemnovichkov.com/keyboard-layout-guide?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov</link><comments>https://blog.artemnovichkov.com/keyboard-layout-guide?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov#comments</comments><dc:creator>artemnovichkov</dc:creator><title>Guide for UIKeyboardLayoutGuide</title><pubDate>Wed, 09 Jun 2021 04:49:10 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/ff/2c/ff2c2cdf-92a1-4cfd-97eb-600faff2e92d.png"></media:content><category>iOS</category><tt:hashtag>uikit</tt:hashtag><tt:hashtag>swift</tt:hashtag><tt:hashtag>ios15</tt:hashtag><description><![CDATA[<img src="https://teletype.in/files/aa/4d/aa4dadb1-2b15-4677-8e84-43b3dd155214.png"></img>The article is now available on my blog:]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p>The article is now available on my blog:</p>
    <p><a href="https://www.artemnovichkov.com/blog/keyboard-layout-guide" target="_blank">https://www.artemnovichkov.com/blog/keyboard-layout-guide</a></p>
  </section>
  <figure class="m_retina">
    <img src="https://teletype.in/files/aa/4d/aa4dadb1-2b15-4677-8e84-43b3dd155214.png" width="600" />
  </figure>
  <p>A common task making app layout is keyboard avoidance. Since iOS 14.0 it works automatically for SwiftUI views. What’s about old, but good UIKit? Previously we used keyboard notifications, checked keyboard height, and updated related constraints. iOS 15 introduces a new layout guide — <code>UIKeyboardLayoutGuide</code>. It’s super intuitive if you’re familiar with other guides like <code>safeAreaLayoutGuide</code> and <code>readableContentGuide</code>. Let’s try to use it in a simple example — we have a login screen with text fields and a login button pinned to the bottom.</p>
  <h2>Base usage</h2>
  <p>We add just two constraints with system spacing:</p>
  <pre data-lang="swift">view.addSubview(loginButton)
let buttonBottom = view.keyboardLayoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: loginButton.bottomAnchor, multiplier: 1.0)
let buttonTrailing = view.keyboardLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: loginButton.trailingAnchor, multiplier: 1.0)
NSLayoutConstraint.activate([buttonBottom, buttonTrailing])</pre>
  <p>Now <code>loginButton</code> layout follows keyboard changes. When the keyboard is offscreen, <code>keyboardLayoutGuide.topAnchor</code> matches the view&#x27;s <code>safeAreaLayoutGuide.bottomAnchor</code>.</p>
  <figure class="m_retina" data-caption-align="center">
    <img src="https://teletype.in/files/a5/d2/a5d2ad22-38ee-44c2-aecf-a10f58de8657.png" width="473" />
    <figcaption><em>Works as simple as it must be</em></figcaption>
  </figure>
  <p>That&#x27;s all, thank you for coming to my TED talk! Wait, the keyboard is no so simple, especially on iPadOS. You can undock and drag it to any place. Luckily, the keyboard guide helps us to handle these cases.</p>
  <figure class="m_retina" data-caption-align="center">
    <img src="https://teletype.in/files/41/18/4118403c-711b-48e2-8ddd-98d6a3569ebb.png" width="638" />
    <figcaption><em>It&#x27;s easier to type with one hand</em></figcaption>
  </figure>
  <h2>Working with floating keyboards</h2>
  <p>At first, we must enable keyboard tracking, it&#x27;s disabled by default:</p>
  <pre data-lang="swift">view.keyboardLayoutGuide.followsUndockedKeyboard = true</pre>
  <p>Now, <code>loginButton</code> starts to follow the keyboard:</p>
  <figure class="m_column" data-caption-align="center">
    <img src="https://teletype.in/files/85/72/8572a4fe-92eb-4b78-99a5-1303c1846560.png" width="524" />
    <figcaption><em>Keyboard is the part of our apps too</em></figcaption>
  </figure>
  <p>It works great, but here we have edge cases. When we move the keyboard at the top, <code>loginButton</code> may be outside of the view frame.</p>
  <p>Actually, <code>UIKeyboardLayoutGuide</code> is a subclass of <code>UITrackingLayoutGuide</code>. It&#x27;s a layout guide that automatically activates and deactivates constraints depending on its nearness to edges. To use it, we replace <code>buttonTrailing</code> constraint with:</p>
  <pre data-lang="swift">let buttonTop = view.keyboardLayoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: loginButton.bottomAnchor, multiplier: 1.0)
buttonTop.identifier = &quot;buttonTop&quot;
view.keyboardLayoutGuide.setConstraints([buttonTop], activeWhenAwayFrom: .top)</pre>
  <p><code>buttonTop</code> constraint will be active only when the keyboard is away from the top. Finally, we add <code>buttonBottom</code> constraint to pin <code>loginButton</code> at the keyboard bottom:</p>
  <pre data-lang="swift">let buttonBottom = loginButton.topAnchor.constraint(equalToSystemSpacingBelow: view.keyboardLayoutGuide.bottomAnchor, multiplier: 1.0)
buttonBottom.identifier = &quot;buttonBottom&quot;
view.keyboardLayoutGuide.setConstraints([buttonBottom], activeWhenNearEdge: .top)</pre>
  <blockquote>Note: configuring identifiers for NSLayoutConstraint allows you to find constraints easily during debugging.</blockquote>
  <p>Here is a final demo of our example. I&#x27;ve added some leading and trailing constraints as well. Check out <a href="https://github.com/artemnovichkov/UIKeyboardLayoutGuideExample" target="_blank">UIKeyboardLayoutGuideExample</a> on Github.</p>
  <figure class="m_column">
    <iframe src="https://www.youtube.com/embed/C2aWOSRhTOM?autoplay=0&loop=0&mute=0"></iframe>
  </figure>
  <h2>Related resources</h2>
  <ul>
    <li><a href="https://support.apple.com/en-us/HT210758" target="_blank">Use the floating keyboard on your iPad</a> by Apple</li>
    <li><a href="https://developer.apple.com/videos/play/wwdc2021/10259" target="_blank">Your guide to keyboard layout</a> by Apple</li>
    <li><a href="https://developer.apple.com/documentation/uikit/keyboards_and_input/adjust_your_layout_with_keyboard_layout_guide" target="_blank">Adjust Your Layout with Keyboard Layout Guide</a> by Apple</li>
    <li><a href="https://fivestars.blog/articles/swiftui-keyboard" target="_blank">SwiftUI keyboard avoidance</a> by <a href="https://twitter.com/zntfdr" target="_blank">Federico Zanetello</a></li>
  </ul>
  <tt-tags>
    <tt-tag name="uikit">#uikit</tt-tag>
    <tt-tag name="swift">#swift</tt-tag>
    <tt-tag name="ios15">#ios15</tt-tag>
  </tt-tags>
  <hr />
  <p data-align="center"><a href="https://twitter.com/iosartem" target="_blank">Twitter</a> · <a href="https://t.me/subtlesettings" target="_blank">Telegram</a> · <a href="http://github.com/artemnovichkov" target="_blank">Github</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.artemnovichkov.com/async-image</guid><link>https://blog.artemnovichkov.com/async-image?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov</link><comments>https://blog.artemnovichkov.com/async-image?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov#comments</comments><dc:creator>artemnovichkov</dc:creator><title>AsyncImage. Loading images in SwiftUI</title><pubDate>Tue, 08 Jun 2021 03:28:32 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/9b/38/9b3888f3-b32a-4d0c-bc93-0c6a9d1f9162.png"></media:content><category>iOS</category><tt:hashtag>swiftui</tt:hashtag><tt:hashtag>ios</tt:hashtag><tt:hashtag>swift</tt:hashtag><tt:hashtag>ios15</tt:hashtag><description><![CDATA[<img src="https://teletype.in/files/c7/43/c7435409-5aa9-4f58-88c5-9a2f654ecc5a.png"></img>The article is now available on my blog:]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p>The article is now available on my blog:</p>
    <p><a href="https://www.artemnovichkov.com/blog/async-image" target="_blank">https://www.artemnovichkov.com/blog/async-image</a></p>
  </section>
  <figure class="m_retina">
    <img src="https://teletype.in/files/c7/43/c7435409-5aa9-4f58-88c5-9a2f654ecc5a.png" width="600" />
  </figure>
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p>Note: Examples are tested on iOS 15.0 with Xcode 13.0 beta (13A5154h).</p>
  </section>
  <p>iOS 15.0 beta gives us new SwiftUI views, and one of them is <code>AsyncImage</code>. It loads and displays an image from the given URL.</p>
  <p>Let&#x27;s start with a basic example:</p>
  <pre data-lang="swift">import SwiftUI

struct ContentView: View {

    private let url = URL(string: &quot;https://picsum.photos/200&quot;)

    var body: some View {
        AsyncImage(url: url)
    }
}</pre>
  <p>By default, it shows a gray background and replaces it with the loaded image:</p>
  <figure class="m_retina" data-caption-align="center">
    <img src="https://teletype.in/files/bf/82/bf823bcc-4363-4728-8e9c-b0c7057fced2.png" width="473" />
    <figcaption><em>Empty and success states of AsyncImage</em></figcaption>
  </figure>
  <p>Optionally we can change <code>scale</code> to use for the image. In the example below the image size will be reduced by half:</p>
  <pre data-lang="swift">import SwiftUI

struct ContentView: View {

    private let url = URL(string: &quot;https://picsum.photos/200&quot;)

    var body: some View {
        AsyncImage(url: url, scale: 2)
    }
}</pre>
  <p>To update the appearance of <code>AsyncImage</code>, we can use an initializer with content and placeholder view builders. Here we able to modify a final image and show a custom placeholder view:</p>
  <pre data-lang="swift">import SwiftUI

struct ContentView: View {

    private let url = URL(string: &quot;https://picsum.photos/200&quot;)

    var body: some View {
        AsyncImage(url: url) { image in
            image
                .resizable()
                .aspectRatio(contentMode: .fit)
        } placeholder: {
            Image(systemName: &quot;photo&quot;)
                .imageScale(.large)
                .foregroundColor(.gray)
        }
        .ignoresSafeArea()
    }
}</pre>
  <figure class="m_retina" data-caption-align="center">
    <img src="https://teletype.in/files/a0/71/a07110ca-10f4-402f-9ed9-f7e9c4fddbbc.png" width="473" />
    <figcaption><em>Custom placeholder and resized image</em></figcaption>
  </figure>
  <p>If we want to handle an error state, we can use another initializer with <code>AsyncImagePhase</code>. It&#x27;s a simple enum with three cases: empty, success, and error.</p>
  <pre data-lang="swift">import SwiftUI

struct ContentView: View {

    private let url = URL(string: &quot;https://picsum.photos/200&quot;)

    var body: some View {
        AsyncImage(url: url, content: view)
    }

    @ViewBuilder
    private func view(for phase: AsyncImagePhase) -&gt; some View {
        switch phase {
        case .empty:
            ProgressView()
        case .success(let image):
            image
                .resizable()
                .aspectRatio(contentMode: .fit)
        case .failure(let error):
            VStack(spacing: 16) {
                Image(systemName: &quot;xmark.octagon.fill&quot;)
                    .foregroundColor(.red)
                Text(error.localizedDescription)
                    .multilineTextAlignment(.center)
            }
        @unknown default:
            Text(&quot;Unknown&quot;)
                .foregroundColor(.gray)
        }
    }
}</pre>
  <p>Here we show a spinner during loading, resized image if loading is successful, and an error message if something is wrong.</p>
  <figure class="m_retina" data-caption-align="center">
    <img src="https://teletype.in/files/4d/8d/4d8da70c-138c-4caa-98ce-ccb9bd219d07.png" width="709.5" />
    <figcaption><em>Working with phases of AsyncImage</em></figcaption>
  </figure>
  <p>To specify animations between phase changes, we can optionally add <code>Transition</code>:</p>
  <pre data-lang="swift">import SwiftUI

struct ContentView: View {

    private let url = URL(string: &quot;https://picsum.photos/200&quot;)
    private let transaction: Transaction = .init(animation: .linear)

    var body: some View {
        AsyncImage(url: url,
                   transaction: transaction,
                   content: view)
    }
    ...
}</pre>
  <p>And, of course, we can use <code>AsyncImage</code> inside <code>List</code> to show multiple images:</p>
  <pre data-lang="swift">import SwiftUI

struct ContentView: View {

    private let url = URL(string: &quot;https://picsum.photos/200&quot;)

    var body: some View {
        List {
            ForEach(0..&lt;10) { _ in
                AsyncImage(url: url,
                           content: view)
                    .listRowInsets(.init(.zero))
            }
        }
        .listStyle(.plain)
    }
    ...
}</pre>
  <figure class="m_retina" data-caption-align="center">
    <img src="https://teletype.in/files/37/fd/37fd64c2-4f2d-4eb7-9300-19fa44f425ff.png" width="473" />
    <figcaption><em>Finally, we can show List without separators </em></figcaption>
  </figure>
  <p>If you want to play with <code>AsyncImage</code> by yourself, check out <a href="https://github.com/artemnovichkov/AsyncImageExample" target="_blank">AsyncImageExample</a> project on Github.</p>
  <h2>References</h2>
  <ul>
    <li><a href="https://developer.apple.com/documentation/SwiftUI/AsyncImage" target="_blank">AsyncImage Documentation</a> by Apple</li>
    <li><a href="https://swiftwithmajid.com/2020/10/07/transactions-in-swiftui" target="_blank">Transactions in SwiftUI</a> by <a href="https://twitter.com/mecid" target="_blank">Majid Jabrayilov</a></li>
  </ul>
  <tt-tags>
    <tt-tag name="swiftui">#swiftui</tt-tag>
    <tt-tag name="ios">#ios</tt-tag>
    <tt-tag name="swift">#swift</tt-tag>
    <tt-tag name="ios15">#ios15</tt-tag>
  </tt-tags>
  <hr />
  <p data-align="center"><a href="https://twitter.com/iosartem" target="_blank">Twitter</a> · <a href="https://t.me/subtlesettings" target="_blank">Telegram</a> · <a href="http://github.com/artemnovichkov" target="_blank">Github</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.artemnovichkov.com/bluetooth-and-swiftui</guid><link>https://blog.artemnovichkov.com/bluetooth-and-swiftui?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov</link><comments>https://blog.artemnovichkov.com/bluetooth-and-swiftui?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov#comments</comments><dc:creator>artemnovichkov</dc:creator><title>Bluetooth and SwiftUI. Developing app for RGB stripe control</title><pubDate>Mon, 31 May 2021 14:31:35 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/e9/e1/e9e1f4df-82d2-48df-afc5-a98b8e3f97b7.png"></media:content><category>iOS</category><tt:hashtag>swiftui</tt:hashtag><tt:hashtag>ios</tt:hashtag><tt:hashtag>swift</tt:hashtag><description><![CDATA[<img src="https://teletype.in/files/22/41/2241fe42-0f44-4703-bc32-9f2f817b2aec.png"></img>The article is now available on my blog:]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p>The article is now available on my blog:</p>
    <p><a href="https://www.artemnovichkov.com/blog/bluetooth-and-swiftui" target="_blank">https://www.artemnovichkov.com/blog/bluetooth-and-swiftui</a></p>
  </section>
  <figure class="m_retina">
    <img src="https://teletype.in/files/22/41/2241fe42-0f44-4703-bc32-9f2f817b2aec.png" width="600" />
  </figure>
  <p>Last year I bought <a href="https://www.ikea.com/us/en/p/bekant-desk-sit-stand-white-s49022538/" target="_blank">BEKANT</a> desk. It looks minimalistic and... boring, so I wanted to add some RGB lightning. I chose a cheap no-name <a href="https://aliexpress.ru/item/32801604250.html" target="_blank">RGB stripe</a> with a controller that works with IR and Bluetooth as well. The seller recommends using <a href="https://apps.apple.com/ru/app/id1145694075" target="_blank">HappyLighting</a> app. It works well enough, supports default styles like pulsating and strobe effects, syncs with music and surround sound. But the interface is a bit strange:</p>
  <figure class="m_retina" data-caption-align="center">
    <img src="https://teletype.in/files/39/be/39beff39-a972-4dff-ba33-ab1d4cf66bdf.jpeg" width="585" />
    <figcaption>Changing colors via  HappyLighting app</figcaption>
  </figure>
  <p>I&#x27;m an iOS developer who learns SwiftUI and Combine, so I decided to write my own app 🧑🏻‍💻. It&#x27;s a proof of concept, but in the future, I can add more features like a watchOS companion app, Siri support, widgets, etc. At the end of the article, you can find a link to a Github repo with source code. Here is a final demo:</p>
  <figure class="m_column">
    <iframe src="https://www.youtube.com/embed/qMGvnPOIhJM?autoplay=0&loop=1&mute=0&playlist=qMGvnPOIhJM"></iframe>
  </figure>
  <h2>Bluetooth: Delegates -&gt; Combine</h2>
  <p>With the power of <code>CoreBluetooth</code> framework, you can check Bluetooth state, scan for peripherals, discover required services and characteristics. By default, it works via Delegate pattern and knows nothing about <code>Combine</code>. If you aren&#x27;t familiar with <code>CoreBluetooth</code>, I recommend checking <a href="http://Core%20Bluetooth%20Tutorial%20for%20iOS:%20Heart%20Rate%20Monitor" target="_blank">this tutorial</a> by Ray Wendenlich team. The basic algorithm for reading and writing data is:</p>
  <ol>
    <li>Find Bluetooth device a.k.a. peripheral.</li>
    <li>Discover its services. Every service is a collection of data related to peripheral features. For instance, heart rate or lightning services.</li>
    <li>Discover characteristics for specific services. Every characteristic provides information about the peripheral state. Also, you can write data for characteristics to update the peripheral states.</li>
  </ol>
  <p>I wrote a simple <code>BluetoothManager</code> object that works with <code>CBCentralManager</code> and <code>CBPeripheral</code> and broadcasts any updates. I decided to use Combine for it. With the magic of publishers, I can subscribe to needed updates and filter/map them in a more declarative way.</p>
  <p>Here is an example of working with states and peripherals:</p>
  <pre data-lang="swift">import Combine
import CoreBluetooth

final class BluetoothManager: NSObject {
    
    private var centralManager: CBCentralManager!
    
    var stateSubject: PassthroughSubject&lt;CBManagerState, Never&gt; = .init()
    var peripheralSubject: PassthroughSubject&lt;CBPeripheral, Never&gt; = .init()
    
    func start() {
        centralManager = .init(delegate: self, queue: .main)
    }
    
    func connect(_ peripheral: CBPeripheral) {
        centralManager.stopScan()
        peripheral.delegate = self
        centralManager.connect(peripheral)
    }
}

extension BluetoothManager: CBCentralManagerDelegate {
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        stateSubject.send(central.state)
    }
    
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        peripheralSubject.send(peripheral)
    }
}</pre>
  <p>In the app views, I subscribe to required subjects and show related states. Here is an example of discovering services:</p>
  <pre data-lang="swift">manager.servicesSubject
    .map { $0.filter { Constants.serviceUUIDs.contains($0.uuid) } }
    .sink { [weak self] services in
        services.forEach { service in
            self?.peripheral.discoverCharacteristics(nil, for: service)
        }
    }
    .store(in: &amp;cancellables)</pre>
  <p>To work with specific devices, you must know identifiers for services and characteristics to read and write data. Unfortunately, my controller has no documentation about its protocol, but I found an <a href="https://github.com/madhead/saberlight/blob/master/protocols/Triones/protocol.md" target="_blank">awesome description on Github</a>. The author reverse-engineered the protocol and described almost all data formats. They look strange and have plenty of magic constants, but who don&#x27;t use them in projects 😅. I added the required identifiers to the app based on the documentation used them for filtering: </p>
  <pre data-lang="swift">enum Constants {
    static let readServiceUUID: CBUUID = .init(string: &quot;FFD0&quot;)
    static let writeServiceUUID: CBUUID = .init(string: &quot;FFD5&quot;)
    static let serviceUUIDs: [CBUUID] = [readServiceUUID, writeServiceUUID]
    static let readCharacteristicUUID: CBUUID = .init(string: &quot;FFD4&quot;)
    static let writeCharacteristicUUID: CBUUID = .init(string: &quot;FFD9&quot;)
}</pre>
  <h2>SwiftUI: states and view models</h2>
  <p>For every view in the app I added view model to split layout and business logic. View models support <code>ObservableObject</code> protocol, work with <code>BluetoothManager</code>, manage subject subscriptions, and have <code>@Published</code> for view updates. The downside of this approach is no convenient way to pass <code>BluetoothManager</code> to every model. Initially, I wanted to use <code>@EnvironmentObject</code> and pass it to all child views, but it works well only in <code>View</code> itself. Finally, I just added it lazily to all view models:</p>
  <pre data-lang="swift">import SwiftUI
import CoreBluetooth
import Combine

final class DevicesViewModel: ObservableObject {
    
    @Published var state: CBManagerState = .unknown
    @Published var peripherals: [CBPeripheral] = []
    
    private lazy var manager: BluetoothManager = .shared
    private lazy var cancellables: Set&lt;AnyCancellable&gt; = .init()
    
    deinit {
        cancellables.cancel()
    }
    
    func start() {
        manager.stateSubject
            .sink { [weak self] state in
                self?.state = state
            }
            .store(in: &amp;cancellables)
        manager.peripheralSubject
            .filter { [weak self] in self?.peripherals.contains($0) == false }
            .sink { [weak self] in self?.peripherals.append($0) }
            .store(in: &amp;cancellables)
        manager.start()
    }
}</pre>
  <p>Views create their own model and handle it with <code>@StateObject</code>:</p>
  <pre data-lang="swift">struct DevicesView: View {
    
    @StateObject private var viewModel: DevicesViewModel = .init()
}</pre>
  <p><a href="http://twitter.com/iosartem" target="_blank">Ping me</a> if you&#x27;re good in SwiftUI dependency injection topic, I really appreciate feedback for this logic.</p>
  <p>Debugging the app, I found that <code>onAppear</code> called twice for some views. People on forums confirm it and fill radars. In the app, I just added a silly check with a state flag:</p>
  <pre data-lang="swift">struct DeviceView: View {
    
    @State private var didAppear = false
    
    var body: some View {
        content()
            .onAppear {
                guard didAppear == false else {
                    return
                }
                didAppear = true
                viewModel.connect()
            }
    }
}</pre>
  <p>I didn&#x27;t want to reinvent the color wheel and used <code>ColorPicker</code> for color selection:</p>
  <pre data-lang="swift">ColorPicker(&quot;Change stripe color&quot;,
            selection: $viewModel.state.color,
            supportsOpacity: false)</pre>
  <p>I used it in UIKit projects a few times, you can just show it on any user action. But in SwiftUI it shows a default interface with no customization.</p>
  <figure class="m_retina" data-caption-align="center">
    <img src="https://teletype.in/files/1c/dd/1cdde876-de9e-48d8-9b6d-7fce7ceeabd2.png" width="585" />
    <figcaption>Yes, I like Dark Mode</figcaption>
  </figure>
  <p>There are two options for selection binding — <code>Color</code> and <code>CGColor</code>. I choose the second one because it is easy to get color components from it.</p>
  <h2>Conclusion</h2>
  <p>The more I use SwiftUI, the more I like its concepts. In pair with Combine, it makes app logic more expressive. And I&#x27;m still sure that developing pet projects are a great way to combine fun and learning.</p>
  <p>The final project is <a href="https://github.com/artemnovichkov/ColorStripe" target="_blank">available on Github</a>. Feel free to check it and share your feedback. Thanks for reading!</p>
  <h2>Related resources</h2>
  <ul>
    <li><a href="https://developer.apple.com/documentation/corebluetooth/transferring_data_between_bluetooth_low_energy_devices" target="_blank">Transferring Data Between Bluetooth Low Energy Devices</a> by Apple</li>
    <li><a href="https://swiftuipropertywrappers.com" target="_blank">SwiftUI Property Wrappers</a> by <a href="https://twitter.com/donnywals" target="_blank">Donny Wals</a></li>
    <li><a href="https://youtu.be/Kp9sHwp4wN8" target="_blank">SwiftUI Color Picker</a> by <a href="https://twitter.com/seanallen_dev" target="_blank">Sean Allen</a></li>
  </ul>
  <p></p>
  <tt-tags>
    <tt-tag name="swiftui">#swiftui</tt-tag>
    <tt-tag name="ios">#ios</tt-tag>
    <tt-tag name="swift">#swift</tt-tag>
  </tt-tags>
  <hr />
  <p data-align="center"><a href="https://twitter.com/iosartem" target="_blank">Twitter</a> · <a href="https://t.me/subtlesettings" target="_blank">Telegram</a> · <a href="http://github.com/artemnovichkov" target="_blank">Github</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.artemnovichkov.com/custom-popups-in-swiftui</guid><link>https://blog.artemnovichkov.com/custom-popups-in-swiftui?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov</link><comments>https://blog.artemnovichkov.com/custom-popups-in-swiftui?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov#comments</comments><dc:creator>artemnovichkov</dc:creator><title>Implementing custom popups in SwiftUI</title><pubDate>Thu, 20 May 2021 13:11:59 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/13/b6/13b60998-0d87-41ba-8070-3e8ff750491f.png"></media:content><category>iOS</category><tt:hashtag>swiftui</tt:hashtag><tt:hashtag>ios</tt:hashtag><tt:hashtag>swift</tt:hashtag><description><![CDATA[<img src="https://teletype.in/files/ed/4a/ed4a64e5-cd82-48b7-96d1-90b9d053fc97.png"></img>The article is now available on my blog:]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p>The article is now available on my blog:</p>
    <p><a href="https://www.artemnovichkov.com/blog/custom-popups-in-swiftui" target="_blank">https://www.artemnovichkov.com/blog/custom-popups-in-swiftui</a></p>
  </section>
  <figure class="m_retina">
    <img src="https://teletype.in/files/ed/4a/ed4a64e5-cd82-48b7-96d1-90b9d053fc97.png" width="600" />
  </figure>
  <p>Two months ago my friend <a href="https://twitter.com/iamnalimov" target="_blank">@iamnalimov </a>and I published jstnmbr app on <a href="https://www.producthunt.com/posts/jstnmbr" target="_blank">ProductHunt</a>. The idea of the app is very simple — count everything: books, push-ups, glasses of water. Of course, I choose SwiftUI for implementation. In this article, I want to highlight the key moments of implementing custom popups. It won&#x27;t be in the tutorial format, we&#x27;ll add a specific layout, but I hope it helps you in using overlays, geometry readers, and modifiers in your projects. If you have any questions or ideas on how to improve it, ping me on <a href="http://twitter.com/iosartem" target="_blank">Twitter</a>.</p>
  <h2>Design is the key</h2>
  <p>Let&#x27;s start with the design. Popups may contain different content, but the appearance and behavior are the same. Superviews are covered with blur overlay, popups have the same background color, top left and right rounded corners, and ability to dismissing:</p>
  <figure class="m_retina" data-caption-align="center">
    <img src="https://teletype.in/files/86/15/861545d0-8f2c-43e8-9ca7-b10f9a2f3673.png" width="207" />
  </figure>
  <p>We want to implement a familiar API interface for presenting like alerts or action sheets:</p>
  <pre data-lang="swift">.popup(isPresented: $isPresented) {
    popupView
}</pre>
  <p>Here we have a <code>Binding&lt;Bool&gt;</code> for presenting state and <code>@ViewBuilder</code> for popup content. Internally it will contain two parts:</p>
  <ol>
    <li>Custom <code>ViewModifier</code> that will show popups via overlays.</li>
    <li>View extension for convenience interface and blur overlays.</li>
  </ol>
  <h2>Modifiers And View Extensions</h2>
  <p>Initially, we create <code>OverlayModifier</code>:</p>
  <pre data-lang="swift">import SwiftUI

struct OverlayModifier&lt;OverlayView: View&gt;: ViewModifier {
    
    @Binding var isPresented: Bool
    @ViewBuilder var overlayView: () -&gt; OverlayView
    
    init(isPresented: Binding&lt;Bool&gt;, @ViewBuilder overlayView: @escaping () -&gt; OverlayView) {
        self._isPresented = isPresented
        self.overlayView = overlayView
    }
}</pre>
  <p>It contains <code>isPresented</code> state and the popup content. To conform <code>ViewModifier</code> protocol, we must implement <code>body(content:)</code> function. In our case it just optionally adds an overlay based on the state:</p>
  <pre data-lang="swift">func body(content: Content) -&gt; some View {
    content.overlay(isPresented ? overlayView() : nil)
}</pre>
  <p>Pay attention to <code>overlayView()</code>. Its body will be called only when popups is presented. <code>View</code> knows nothing about this modifier, so we extend <code>View</code> protocol with popup presentation:</p>
  <pre data-lang="swift">extension View {
    
    func popup&lt;OverlayView: View&gt;(isPresented: Binding&lt;Bool&gt;,
                                  blurRadius: CGFloat = 3,
                                  blurAnimation: Animation? = .linear,
                                  @ViewBuilder overlayView: @escaping () -&gt; OverlayView) -&gt; some View {
        blur(radius: isPresented.wrappedValue ? blurRadius : 0)
            .animation(blurAnimation)
            .allowsHitTesting(!isPresented.wrappedValue)
            .modifier(OverlayModifier(isPresented: isPresented, overlayView: overlayView))
    }
}</pre>
  <p>Let&#x27;s describe every modifier:</p>
  <ol>
    <li><code>blur</code> adds a blur overlay to superview if the popup is presented. We have a default value in function parameters to reuse the same radius and modify for specific popups if needed.</li>
    <li><code>animation</code> adds an animation for blur overlay if needed.</li>
    <li><code>allowsHitTesting</code> disables user interactions in superview if the popup is presented.</li>
    <li><code>modifier</code> applies custom <code>OverlayModifier</code> with passed <code>overlayView</code>.</li>
  </ol>
  <p>We&#x27;re ready to show popups, but we don&#x27;t have any yet 😅. Let&#x27;s make a basic <code>PopupView</code> with a common appearance according to our goals:</p>
  <ul>
    <li>It may contain different content inside;</li>
    <li>It has a background color and rounded top left and top right corners;</li>
    <li>It is showed from the bottom with animation.</li>
  </ul>
  <h2>Popup Layout</h2>
  <p>Let&#x27;s create a simple view named <code>NamePopupView</code>  that knows nothing about popup logic:</p>
  <figure class="m_retina" data-caption-align="center">
    <img src="https://teletype.in/files/52/74/52743ba3-3e94-40d9-bfcc-73705e4c415f.png" width="522" />
    <figcaption><em>You can check the implementation in the example project.</em></figcaption>
  </figure>
  <p>The app may show different popups, so we create a reusable <code>BottomPopupView</code> to show different content:</p>
  <pre data-lang="swift">struct BottomPopupView&lt;Content: View&gt;: View {
    
    @ViewBuilder var content: () -&gt; Content
    
    var body: some View {
        content()
    }
}</pre>
  <p>Now we can show it in <code>.popup</code> modifier:</p>
  <pre data-lang="swift">.popup(isPresented: $isPresented) {
	BottomPopupView {
		NamePopupView(isPresented: $isPresented)
	}
}</pre>
  <p>By default, overlays are shown in the center of the superview. To pin it to the bottom we wrap the content into <code>VStack</code> with <code>Spacer</code>:</p>
  <pre data-lang="swift">VStack {
	Spacer()
	content()
	    .background(Color.white)
	    .cornerRadius(radius: 16, corners: [.topLeft, .topRight])
}</pre>
  <p>Default <code>cornerRadius</code> modifier works for all corners, so here we use a custom modifier for it:</p>
  <pre data-lang="swift">struct RoundedCornersShape: Shape {
    
    let radius: CGFloat
    let corners: UIRectCorner
    
    func path(in rect: CGRect) -&gt; Path {
        let path = UIBezierPath(roundedRect: rect,
                                byRoundingCorners: corners,
                                cornerRadii: CGSize(width: radius, height: radius))
        return Path(path.cgPath)
    }
}

extension View {
    
    func cornerRadius(radius: CGFloat, corners: UIRectCorner = .allCorners) -&gt; some View {
        clipShape(RoundedCornersShape(radius: radius, corners: corners))
    }
}</pre>
  <p>All is now ready, and here is the result:</p>
  <figure class="m_retina">
    <img src="https://teletype.in/files/bb/6b/bb6b94a9-0de9-48af-a6fe-25642dbe9b99.png" width="473" />
  </figure>
  <p>Insets for Safe Area have added automatically, but we want to overlay superviews at the bottom too. To read and use Safe Area insets, we add GeometryReader:</p>
  <pre data-lang="swift">GeometryReader { geometry in
	VStack {
	    Spacer()
	    content()
		    .padding(.bottom, geometry.safeAreaInsets.bottom)
		    .background(Color.white)
		    .cornerRadius(radius: 16, corners: [.topLeft, .topRight])
	}
	.edgesIgnoringSafeArea([.bottom])
}</pre>
  <p>To pin our popup at the bottom, we add <code>.edgesIgnoringSafeArea</code> modifier. According to the content, we add a bottom padding with the bottom inset before <code>.background</code> modifier. With this logic background color will appear as expected.</p>
  <figure class="m_retina">
    <img src="https://teletype.in/files/87/fb/87fba30d-a5ee-47ad-946c-bcf3fa627877.png" width="473" />
  </figure>
  <p>Since iOS 14 we even have an automatic keyboard avoidance:</p>
  <figure class="m_retina">
    <img src="https://teletype.in/files/95/99/95995056-6237-431d-b1d5-2fe74931393d.png" width="473" />
  </figure>
  <h2>Animations</h2>
  <p>The layout is finished 🥳, but there is no animation. Luckily SwiftUI has easy-to-use modifiers for animations and transitions:</p>
  <pre data-lang="swift">GeometryReader { geometry in
    ...
}
.animation(.easeOut)
.transition(.move(edge: .bottom))</pre>
  <figure class="m_retina">
    <img src="https://teletype.in/files/e1/ff/e1ffb40c-55df-433c-a6af-04ac316473f8.gif" width="300" />
  </figure>
  <h2>Source Code</h2>
  <p>You can find the final project on <a href="https://github.com/artemnovichkov/CustomPopupExample" target="_blank">Github</a>. Thanks for reading!</p>
  <h2>Related Articles</h2>
  <ul>
    <li><a href="https://www.vadimbulavin.com/swiftui-popup-sheet-popover/" target="_blank">Custom Popup in SwiftUI</a> by <a href="https://twitter.com/V8tr" target="_blank">Vadim Bulavin</a></li>
    <li><a href="https://swiftwithmajid.com/2020/11/04/how-to-use-geometryreader-without-breaking-swiftui-layout/" target="_blank">How to use GeometryReader without breaking SwiftUI layout</a> by <a href="https://twitter.com/mecid" target="_blank">Majid Jabrayilov</a></li>
    <li><a href="https://www.fivestars.blog/articles/swiftui-keyboard/" target="_blank">SwiftUI keyboard avoidance</a> by <a href="https://twitter.com/zntfdr" target="_blank">Federico Zanetello</a></li>
  </ul>
  <tt-tags>
    <tt-tag name="swiftui">#swiftui</tt-tag>
    <tt-tag name="ios">#ios</tt-tag>
    <tt-tag name="swift">#swift</tt-tag>
  </tt-tags>
  <hr />
  <p data-align="center"><a href="https://twitter.com/iosartem" target="_blank">Twitter</a> · <a href="https://t.me/subtlesettings" target="_blank">Telegram</a> · <a href="http://github.com/artemnovichkov" target="_blank">Github</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.artemnovichkov.com/result-builders</guid><link>https://blog.artemnovichkov.com/result-builders?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov</link><comments>https://blog.artemnovichkov.com/result-builders?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov#comments</comments><dc:creator>artemnovichkov</dc:creator><title>Using result builders for action sheets in SwiftUI</title><pubDate>Sat, 01 May 2021 13:21:47 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/2c/c6/2cc62750-2429-4ce6-8a2f-a8be0079f125.png"></media:content><category>iOS</category><tt:hashtag>swiftui</tt:hashtag><tt:hashtag>ios</tt:hashtag><tt:hashtag>swift</tt:hashtag><description><![CDATA[<img src="https://teletype.in/files/59/f2/59f27b82-e31f-464b-b1f8-6eda4b232447.png"></img>The article is now available on my blog:]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p>The article is now available on my blog:</p>
    <p><a href="https://www.artemnovichkov.com/blog/result-builders" target="_blank">https://www.artemnovichkov.com/blog/result-builders</a></p>
  </section>
  <figure class="m_column">
    <img src="https://teletype.in/files/59/f2/59f27b82-e31f-464b-b1f8-6eda4b232447.png" width="1200" />
  </figure>
  <p>One of the key features of SwiftUI is a declarative syntax for layout. It is available thanks to <strong>result builders</strong>, previously called <em>function builders</em>. With result builders, we can implicitly build up a final value from a sequence of components. The final revision of this feature is released in Swift 5.4, and Xcode 12.5 suggests code completions and fix-its for it. I guess it&#x27;s a good sign for exploring it and making action sheets more declarative!</p>
  <h2>Preparation</h2>
  <p>We&#x27;ll create a simple SwiftUI app, where we can select ingredients for sandwich.</p>
  <pre data-lang="swift">struct ContentView: View {
    
    @State private var ingredients: [String] = []
    @State private var isActionSheetPresented = false
    
    var body: some View {
        VStack {
            Text(ingredients.joined())
                .font(.system(.title))
            Button(&quot;Make a sandwich&quot;) {
                isActionSheetPresented = true
            }
        }
        .padding()
        .actionSheet(isPresented: $isActionSheetPresented) {
            let buttons = [ActionSheet.Button.default(Text(&quot;🍞&quot;)) {
                ingredients.append(&quot;🍞&quot;)
            },
            ActionSheet.Button.cancel()]
            return ActionSheet(title: Text(&quot;Select an ingredient&quot;), message: nil, buttons: buttons)
        }
    }
}</pre>
  <p>When we tap on the button, <code>ActionSheet</code> is presented with buttons from the array in the initializer. The syntax for action buttons, especially with defined actions, looks a bit complicated. Let&#x27;s improve it with a custom result builder.</p>
  <figure class="m_retina" data-caption-align="center">
    <img src="https://teletype.in/files/0e/16/0e16d6d0-9db3-47c0-9c95-77a00e9b6e63.png" width="473" />
    <figcaption>Half a loaf is better than no bread.</figcaption>
  </figure>
  <h2>Basics</h2>
  <p>We create a <code>ButtonsBuilder</code> struct with <code>@resultBuilder</code> attribute. To start using it, we must implement at least one static <code>buildBlock</code> function:</p>
  <pre data-lang="swift">@resultBuilder
struct ButtonsBuilder {

    static func buildBlock(_ components: ActionSheet.Button...) -&gt; [ActionSheet.Button] {
        components
    }
}</pre>
  <p>Here we have a variadic parameter with <code>ActionSheet.Button</code> and just return it as is.</p>
  <p>Because <code>ActionSheet</code> knows nothing about our builder, we create a new initializer with title, message, and the builder:</p>
  <pre data-lang="swift">extension ActionSheet {
    
    init(title: Text, message: Text? = nil, @ButtonsBuilder buttons: () -&gt; [ActionSheet.Button]) {
        self.init(title: title, message: message, buttons: buttons())
    }
}</pre>
  <p>Now we&#x27;re ready to refactor <code>ActionSheet</code> configuration:</p>
  <pre data-lang="swift">.actionSheet(isPresented: $isActionSheetPresented) {
    ActionSheet(title: Text(&quot;Select an ingredient&quot;), message: nil) {
        ActionSheet.Button.default(Text(&quot;🍞&quot;)) {
            ingredients.append(&quot;🍞&quot;)
        }
        ActionSheet.Button.cancel()
    }
}</pre>
  <p>Looks great!</p>
  <h2>What if?.. Working with conditions</h2>
  <p>Result builders may build a partial result depending on some conditions. In our app, we add a new <code>State</code> and <code>Toggle</code> . If it is enabled, we add cucumbers and tomatoes otherwise. </p>
  <pre data-lang="swift">// In States section
@State private var likeCucumbers = true

// Below Text in ContentView
Toggle(&quot;I love cucumbers&quot;, isOn: $likeCucumbers)</pre>
  <p>To support <code>if-else</code> conditions in our builder, we must implement <code>buildEither(first:)</code> and <code>buildEither(second:)</code> functions:</p>
  <pre data-lang="swift">@resultBuilder
struct ButtonsBuilder {
    
    ...
    
    static func buildEither(first components: [ActionSheet.Button]) -&gt; [ActionSheet.Button] {
        components
    }
    
    static func buildEither(second components: [ActionSheet.Button]) -&gt; [ActionSheet.Button] {
        components
    }
}</pre>
  <p>If we try to add if-else statement like this:</p>
  <pre data-lang="swift">if likeCucumbers {
	ActionSheet.Button.default(Text(&quot;🥒&quot;)) {
		ingredients.append(&quot;🥒&quot;)
	}
}
else {
	ActionSheet.Button.default(Text(&quot;🍅&quot;)) {
		ingredients.append(&quot;🍅&quot;)
	}
}</pre>
  <p>We have an error:</p>
  <blockquote> Cannot pass array of type &#x27;[ActionSheet.Button]&#x27; (aka &#x27;Array&lt;Alert.Button&gt;&#x27;) as variadic arguments of type &#x27;ActionSheet.Button&#x27; (aka &#x27;Alert.Button&#x27;)</blockquote>
  <p>We can solve the error by defining a new protocol and implementing it by both a single <code>ActionSheet.Button</code> and a collection of <code>ButtonsConvertible</code>:</p>
  <pre data-lang="swift">protocol ButtonsConvertible {
    
    var buttons: [ActionSheet.Button] { get }
}

extension ActionSheet.Button: ButtonsConvertible {
    
    var buttons: [ActionSheet.Button] {
        [self]
    }
}

extension Array: ButtonsConvertible where Element == ButtonsConvertible {

    var buttons: [ActionSheet.Button] { self.flatMap(\.buttons) }
}</pre>
  <p>In <code>ButtonsBuilder</code> we replace all <code>ActionSheet.Button</code> with <code>ButtonsConvertible</code>. And finally, we implement <code>buildFinalResult</code> function that gets all <code>ButtonsConvertible</code> and maps it to buttons:</p>
  <pre data-lang="swift">@resultBuilder
struct ButtonsBuilder {
    
    static func buildBlock(_ components: ButtonsConvertible...) -&gt; [ButtonsConvertible] {
        components
    }
    
    ...
    
    static func buildFinalResult(_ components: [ButtonsConvertible]) -&gt; [ActionSheet.Button] {
        components.flatMap(\.buttons)
    }
}</pre>
  <p>Now <code>likeCucumbers</code> check builds successfully.</p>
  <h2>Using ForEach for Actions</h2>
  <p>SwiftUI has an awesome <code>ForEach</code> element. It gets different data collections and converts them to views via <code>@ViewBuilder</code>. I was wondering if there is any chance to use it for buttons 🤔. Of course, let&#x27;s start with an extension:</p>
  <pre data-lang="swift">extension ForEach: ButtonsConvertible where Content == ActionSheet.Button {

    var buttons: [ActionSheet.Button] {
        data.map(content)
    }
}</pre>
  <p>Here we declare that <code>Content</code> generic must be <code>ActionSheet.Button</code> and map data to buttons via <code>content</code> closure.</p>
  <p><code>ActionSheet.Button</code> is a simple typealias for <code>Alert.Button</code>, and <code>Alert.Button</code> is just a struct that doesn&#x27;t conform <code>View</code> protocol. To solve it, we implement it and return <code>Never</code> for the body:</p>
  <pre data-lang="swift">extension ActionSheet.Button: View {

    public var body: Never {
        fatalError()
    }
}</pre>
  <p>Because we don&#x27;t use <code>ForEach</code> for rendering, the body will never be called. And it works now!</p>
  <pre data-lang="swift">ForEach([&quot;🧅&quot;, &quot;🧄&quot;], id: \.self) { string in
	ActionSheet.Button.default(Text(string)) {
		ingredients.append(string)
	}
}</pre>
  <p>We can explicitly add ids like in the example, use <code>Identifiable</code> array or even ranges inside <code>ForEach</code>. The downside of this trick is that we can accidentally use <code>ActionSheet.Button</code> inside any body and get <code>fatalError</code> in runtime.</p>
  <h2>Conclusion</h2>
  <p>Result builder is a great enhancement in Swift language. In certain cases, it improves code readability dramatically. If you want to play with the example, check <a href="https://github.com/artemnovichkov/ResultBuilderExample" target="_blank">ResultBuilderExample</a> repo.</p>
  <p>Thanks for reading 🙏</p>
  <h2>References</h2>
  <ul>
    <li><a href="https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID633" target="_blank">Official documentation</a> by Apple</li>
    <li><a href="https://www.avanderlee.com/swift/result-builders/" target="_blank">Result builders in Swift explained with code examples</a> by <a href="https://twitter.com/twannl" target="_blank">Antoine v.d. SwiftLee</a></li>
    <li><a href="https://github.com/carson-katri/awesome-function-builders" target="_blank">awesome-function-builders</a> by community</li>
  </ul>
  <tt-tags>
    <tt-tag name="swiftui">#swiftui</tt-tag>
    <tt-tag name="ios">#ios</tt-tag>
    <tt-tag name="swift">#swift</tt-tag>
  </tt-tags>
  <hr />
  <p data-align="center"><a href="https://twitter.com/iosartem" target="_blank">Twitter</a> · <a href="https://t.me/subtlesettings" target="_blank">Telegram</a> · <a href="http://github.com/artemnovichkov" target="_blank">Github</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.artemnovichkov.com/swiftui-offline</guid><link>https://blog.artemnovichkov.com/swiftui-offline?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov</link><comments>https://blog.artemnovichkov.com/swiftui-offline?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov#comments</comments><dc:creator>artemnovichkov</dc:creator><title>Working with web content offline in SwiftUI apps</title><pubDate>Sun, 25 Apr 2021 08:39:25 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/5c/c0/5cc0a578-21ef-4b7d-b4df-cc293811c759.png"></media:content><category>iOS</category><tt:hashtag>swiftui</tt:hashtag><tt:hashtag>ios</tt:hashtag><tt:hashtag>swift</tt:hashtag><description><![CDATA[<img src="https://teletype.in/files/28/a1/28a1cbe3-6639-473e-9215-d1177679e767.png"></img>The article is now available on my blog:]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p>The article is now available on my blog:</p>
    <p><a href="https://www.artemnovichkov.com/blog/swiftui-offline" target="_blank">https://www.artemnovichkov.com/blog/swiftui-offline</a></p>
  </section>
  <figure class="m_column">
    <img src="https://teletype.in/files/28/a1/28a1cbe3-6639-473e-9215-d1177679e767.png" width="1200" />
  </figure>
  <p>I continue developing an app for saving and reading articles. In my <a href="https://blog.artemnovichkov.com/sheet-happens" target="_blank">previous post</a>, I covered interesting cases of using sheets in SwiftUI. Now I want to describe my journey with offline mode.</p>
  <p>There are situations when users are unable to download web content: bad connection, airplane mode, etc. <code>WKWebView</code> has useful APIs for saving its content in different formats. Let&#x27;s check it out!</p>
  <blockquote>Note: Examples are written in Swift 5.4 and tested on iOS 14.5 with Xcode 12.5 (12E262).</blockquote>
  <h2>Preparation</h2>
  <p>In the app user can save URL content without explicit previewing. To deal with it, we use a simple <code>WKWebView</code> wrapper called <code>WebDataManager</code>:</p>
  <pre data-lang="swift">import WebKit

final class WebDataManager: NSObject {
    
    private lazy var webView: WKWebView = {
        let webView = WKWebView()
        webView.navigationDelegate = self
        return webView
    }()
    
    private var completionHandler: ((Result&lt;Data, Error&gt;) -&gt; Void)?
    
    func createData(url: URL, completionHandler: @escaping (Result&lt;Data, Error&gt;) -&gt; Void) {
        self.completionHandler = completionHandler
        webView.load(.init(url: url))
    }
}</pre>
  <p>We create a web view without frame, because we don&#x27;t want to show it. Also, <code>WebDataManager</code> has a convenience function with completion to handle web content. For all formats it will return <code>Data</code> that we can store locally.</p>
  <p>To work with web navigation, we must conform <code>WKNavigationDelegate</code> and implement <code>didFinish</code> and <code>didFail</code> functions:</p>
  <pre data-lang="swift">extension WebDataManager: WKNavigationDelegate {
    
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        // save loaded content
    }
    
    func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        completionHandler?(.failure(error))
    }
}</pre>
  <p>These functions will be called on navigation changes. Now we are ready to save the content, and our first station will be <code>takeSnapshot</code>.</p>
  <h2>Snapshots</h2>
  <p>Since iOS 11.0 <code>WKWebView</code> has <code>takeSnapshot</code> function. It has an optional <code>WKSnapshotConfiguration</code> to specify a capture behavior. In the final, it returns a generated image.</p>
  <pre data-lang="swift">// declared in WebDataManager
enum DataError: Error {
    case noImageData
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    let config = WKSnapshotConfiguration()
    webView.takeSnapshot(with: config) { [weak self] image, error in
        if let error = error {
            self?.completionHandler?(.failure(error))
            return
        }
        guard let pngData = image?.pngData() else {
            self?.completionHandler?(.failure(DataError.noImageData))
            return
        }
        self?.completionHandler?(.success(pngData))
    }
}</pre>
  <p>Remember zero frame of the web view? Because of it, we have an unknown error here:</p>
  <blockquote>Error Domain=WKErrorDomain Code=1 &quot;An unknown error occurred&quot; UserInfo={NSLocalizedDescription=An unknown error occurred}</blockquote>
  <figure class="m_column">
    <img src="https://teletype.in/files/07/85/07859f30-af4f-42d4-a73f-93b62c5377dd.gif" width="480" />
  </figure>
  <p>To fix it, we can get a <code>contentSize</code> and set it to config&#x27;s rect:</p>
  <pre data-lang="swift">config.rect = .init(origin: .zero, size: webView.scrollView.contentSize)</pre>
  <p>Now we have the image data, can save it locally and show in the app:</p>
  <pre data-lang="swift">import SwiftUI

struct SnapshotContentView: View {
    
    let url: URL
    
    var body: some View {
        if let image = UIImage(contentsOfFile: url.path) {
            ScrollView {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
            }
        }
        else {
            Text(&quot;Fail to load image&quot;)
        }
    }
}</pre>
  <p>Let&#x27;s highlight a few cons of this approach:</p>
  <ul>
    <li>Images have fixed sizes and may look bad on different screens;</li>
    <li>We can&#x27;t copy text from contents and open URLs.</li>
  </ul>
  <p>Luckily, the next approach has no these issues.</p>
  <h2>PDF</h2>
  <p>With iOS 14.0 <code>WKWebView</code> got a new <code>createPDF</code> function. It already returns a <code>Result</code> object that we can just pass to our <code>completionHandler</code>:</p>
  <pre data-lang="swift">func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    webView.createPDF { [weak self] result in
        self?.completionHandler?(result)
    }
}</pre>
  <p>For a record, the function also takes <code>WKPDFConfiguration</code> object with the only option — a rect to capture a portion of the web view.</p>
  <p>SwiftUI has no views for PDF content. But with helping of <code>UIViewRepresentable</code> we can use <code>PDFView</code> from <code>PDFKit</code> framework:</p>
  <pre data-lang="swift">import SwiftUI
import PDFKit

struct PDFContentView: UIViewRepresentable {
    
    let url: URL
    
    func makeUIView(context: Context) -&gt; PDFView {
        let view = PDFView()
        view.autoScales = true
        view.document = PDFDocument(url: url)
        return view
    }
    
    func updateUIView(_ pdfView: PDFView, context: Context) {
    }
}</pre>
  <p>Now we can copy text and even open URLs in Safari, but web content is still static. For instance, animations are gone, drop-down lists will be collapsed forever.</p>
  <h2>Web archive</h2>
  <p>The last approach in this article is web archives. A web archive is a file that archives inside it all the content of one web page. And since iOS 14.0 we can work with it easily:</p>
  <pre data-lang="swift">func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    webView.createWebArchiveData { [weak self] result in
        self?.completionHandler?(result)
    }
}</pre>
  <p>Of course, we have another wrapper, but for <code>WKWebView</code>:</p>
  <pre data-lang="swift">import SwiftUI
import WebKit

struct WebArchiveContentView: UIViewRepresentable {
    
    let url: URL
    
    func makeUIView(context: Context) -&gt; WKWebView {
        WKWebView()
    }
    
    func updateUIView(_ webView: WKWebView, context: Context) {
        webView.loadFileURL(url, allowingReadAccessTo: url)
    }
}</pre>
  <p>With the magic of web archives web page logic is working as well. With an internet connection, the web view will navigate to tapped content links.</p>
  <h2>Conclusion</h2>
  <p>Finally, for the app I choose web archives, but all approaches are helpful based on app purposes. The last thing I want to add is a size of rendered contents. For my blog&#x27;s main page the sizes are:</p>
  <ul>
    <li>.png — 1,8 Mb;</li>
    <li>.pdf — 2,3 Mb;</li>
    <li>.webarchive — 5,3 Mb.</li>
  </ul>
  <p>I don&#x27;t mind it (yet 😅), but I&#x27;m planning to add features for clearing saved data and download archives for selected articles.</p>
  <p>If you want to play with examples and test your URLs, an example project with all three saving options is available <a href="https://github.com/artemnovichkov/sheet" target="_blank">here</a>.</p>
  <figure class="m_retina" data-caption-align="center">
    <img src="https://teletype.in/files/6d/79/6d79e1cb-54ae-4b8f-967d-915f80496846.png" width="473" />
    <figcaption>An example of web archive preview</figcaption>
  </figure>
  <tt-tags>
    <tt-tag name="swiftui">#swiftui</tt-tag>
    <tt-tag name="ios">#ios</tt-tag>
    <tt-tag name="swift">#swift</tt-tag>
  </tt-tags>
  <hr />
  <p data-align="center"><a href="https://twitter.com/iosartem" target="_blank">Twitter</a> · <a href="https://t.me/subtlesettings" target="_blank">Telegram</a> · <a href="http://github.com/artemnovichkov" target="_blank">Github</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.artemnovichkov.com/sheet-happens</guid><link>https://blog.artemnovichkov.com/sheet-happens?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov</link><comments>https://blog.artemnovichkov.com/sheet-happens?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov#comments</comments><dc:creator>artemnovichkov</dc:creator><title>Sheet happens. Working with modal views in SwiftUI</title><pubDate>Wed, 14 Apr 2021 14:02:42 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/e5/37/e5379a2e-433e-4802-96dc-6f669cd316e5.png"></media:content><category>iOS</category><tt:hashtag>swiftui</tt:hashtag><tt:hashtag>ios</tt:hashtag><tt:hashtag>swift</tt:hashtag><description><![CDATA[<img src="https://teletype.in/files/86/35/86354351-a8d8-4467-874f-5bf23847168f.png"></img>The article is now available on my blog:]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p>The article is now available on my blog:</p>
    <p><a href="https://www.artemnovichkov.com/blog/sheet-happens" target="_blank">https://www.artemnovichkov.com/blog/sheet-happens</a></p>
  </section>
  <figure class="m_column">
    <img src="https://teletype.in/files/86/35/86354351-a8d8-4467-874f-5bf23847168f.png" width="1200" />
  </figure>
  <p>Developing pet projects is one of the best ways to learn new things. Since SwiftUI 1.0 I&#x27;ve been writing an app for saving and reading articles. I rewrote it twice from scratch, added and removed features based on my user experience, used new APIs, etc.</p>
  <p>I had a few interesting cases implementing modal views a.k.a. sheets in SwiftUI. The main goal of this article is to summarize my experience and use it as a reference for the future. I simplify code examples to focus on specific logic. Let&#x27;s start with showing.</p>
  <blockquote>Note: Examples are written in Swift 5.3.2 and work on iOS 14.0 with Xcode 12.4 (12D4e).</blockquote>
  <h2>Showing sheets</h2>
  <p>There are two options for showing sheets. </p>
  <p>The first one is based on <code>Binding&lt;Bool&gt;</code> value. It can be stored via <code>@State</code> property wrapper and good for sheets that need no external data:</p>
  <pre data-lang="swift">struct ArticlesView: View {

    @State var isSettingsViewPresented = false
    
    var body: some View {
        Button(&quot;Show Settings&quot;) {
            isSettingsViewPresented = true
        }
        .sheet(isPresented: $isSettingsViewPresented) {
            SettingsView()
        }
    }
}

struct SettingsView: View {

    var body: some View {
        Text(&quot;Settings&quot;)
    }
}</pre>
  <p>The second one is based on <code>Binding&lt;Item?&gt;</code> value where the item is an optional <code>Identifiable</code> object. it&#x27;s a good choice if we want to pass any data to sheets. For instance, a selected article for reading. Pay attention that the selected article is not optional in sheet content closure:</p>
  <pre data-lang="swift">// A simple struct with article data
struct Article: Identifiable {

    let id: UUID
    let title: String
}

struct ArticlesView: View {

    @State var article: Article?
    
    var body: some View {
        Button(&quot;Show Article&quot;) {
            article = .init(id: .init(), title: &quot;Awesome article&quot;)
        }
        .sheet(item: $article) { article in
            ArticleDetailsView(article: article)
        }
    }
}

// A view for article showing
struct ArticleDetailsView: View {

    @State var article: Article
    
    var body: some View {
        Text(article.title)
    }
}</pre>
  <p><strong>An interesting moments:</strong></p>
  <ul>
    <li>if <code>id</code> of the selected article will be changed, the presented sheet will be dismissed and replaced with a new sheet;</li>
    <li>if the selected article becomes nil at any moment, the sheet will be dismissed as well. </li>
  </ul>
  <p>For both showing options we can optionally add <code>onDismiss</code> closure that executed when the sheet dismisses:</p>
  <pre data-lang="swift">struct ArticlesView: View {

    @State var article: Article?
    
    var body: some View {
        Button(&quot;Show Article&quot;) {
            article = .init(id: .init(), title: &quot;Awesome article&quot;)
        }
        .sheet(item: $article, onDismiss: onDismiss) { article in
            ArticleDetailsView(article: article)
        }
    }
       
    private func onDismiss() {
        print(&quot;dismisses&quot;)
    }
}</pre>
  <p>By default, we can dismiss sheets via swipe gestures. But what if we want to dismiss sheets based on another user action or app logic?</p>
  <h2>Dismissing sheets</h2>
  <p>There are two options to dismiss sheets too. If we use <code>Binding&lt;Bool&gt;</code>, we can pass it to a sheet via <code>@Binding</code> property wrapper and update its value. Bindings allow to change values in parent views:</p>
  <pre data-lang="swift">// in ArticlesView
.sheet(isPresented: $isSettingsViewPresented) {
    SettingsView(isSettingsViewPresented: $isSettingsViewPresented)
}

struct SettingsView: View {
    
    @Binding var isSettingsViewPresented: Bool
    
    var body: some View {
        ZStack {
            Text(&quot;Settings&quot;)
            VStack {
                Spacer()
                Button(&quot;Dismiss&quot;) {
                    isSettingsViewPresented = false
                }
            }
        }
    }
}</pre>
  <p>Another option is more universal and based on <code>presentationMode</code> environment:</p>
  <pre data-lang="swift">struct SettingsView: View {
    
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        ZStack {
            Text(&quot;Settings&quot;)
            VStack {
                Spacer()
                Button(&quot;Dismiss&quot;) {
                    presentationMode.wrappedValue.dismiss()
                }
            }
        }
    }
}</pre>
  <p>This environment indicates whether a view is currently presented by another view.</p>
  <h2>Sheets and ForEach</h2>
  <p>The app with only one article is a bit useless, so let&#x27;s create <code>List</code> and make articles rows via <code>ForEach</code>:</p>
  <pre data-lang="swift">struct ArticlesView: View {

    let articles: [Article] = [.init(id: .init(), title: &quot;Awesome article&quot;),
                               .init(id: .init(), title: &quot;Another awesome article&quot;)]
    @State var isArticleDetailsViewPresented: Bool = false
    
    var body: some View {
        NavigationView {
            List {
                ForEach(articles) { article in
                    Text(article.title)
                        .onTapGesture {
                            isArticleDetailsViewPresented = true
                        }
                        .sheet(isPresented: $isArticleDetailsViewPresented) {
                            ArticleDetailsView(article: article)
                        }
                }
            }
            .navigationTitle(&quot;Articles&quot;)
        }
    }
}</pre>
  <p>Here we use <code>Binding&lt;Bool&gt;</code> for sheet state and update its value on tap action. Because we have the articles in enumeration, we can pass it to <code>ArticleDetailsView</code>. Everything looks fine, the app builds successfully. But <code>ArticleDetailsView</code> presents only with <code>&quot;Awesome article&quot;</code> title for the first article. Ok, remember this case and try to update sheets logic in the next article section after another example.</p>
  <h2>Multiple sheets</h2>
  <p>Previously we used one sheet for the selected article. In my app, I show different views according to the internet connection. If the internet is available, I show <code>SafariView</code> with the url of the selected article, and <code>WebArchiveView</code> with saved web archive data.</p>
  <p>At first, we update our models:</p>
  <pre data-lang="swift">struct Article: Identifiable {

    let id: UUID
    let title: String
    let url: URL
}

extension URL: Identifiable {
    
    public var id: String {
        absoluteString
    }
}

extension String: Identifiable {
    
    public var id: String {
        self
    }
}</pre>
  <p>URL and String types must conform <code>Identifiable</code> protocol for using in sheet bindings.</p>
  <p>Next, we update ArticlesView with multiple sheets. To simplify the example let&#x27;s show different views based on <code>Bool</code> state:</p>
  <pre data-lang="swift">struct ArticlesView: View {

    let article: Article = .init(id: .init(),
                                 title: &quot;Sheet happens&quot;,
                                 url: URL(string: &quot;https://blog.artemnovichkov.com/editor/sheet-happens&quot;)!)
    
    @State var url: URL?
    @State var title: String?
    @State var isOn = true
    
    var body: some View {
        HStack {
            Button(&quot;Show Article&quot;) {
                if isOn {
                    url = article.url
                }
                else {
                    title = article.title
                }
            }
            .sheet(item: $url) { url in
                SafariView(url: url)
            }
            .sheet(item: $title) { title in
                WebArchiveView(title: title)
            }
            Toggle(&quot;&quot;, isOn: $isOn)
        }
        .padding()
    }
}</pre>
  <p>Finally, we add views with different states and body content:</p>
  <pre data-lang="swift">struct SafariView: View {
    
    @State var url: URL
    
    var body: some View {
        Text(&quot;Content of &quot; + url.absoluteString)
    }
}

struct WebArchiveView: View {
    
    @State var title: String
    
    var body: some View {
        Text(&quot;Web archive data of \(title) article&quot;)
    }
}</pre>
  <p>Here we have another strange behavior — only <code>WebArchiveView</code> will be shown. It&#x27;s a known issue in SwiftUI, but don&#x27;t give up, let&#x27;s fix it!</p>
  <h2>Fixing multiple sheets</h2>
  <p>We create <code>Sheet</code> enum with cases for every sheet. It must conform <code>Identifiable</code> as well:</p>
  <pre data-lang="swift">enum Sheet: Identifiable {
    
    case url(URL)
    case webArchive(String)
    
    var id: String {
        switch self {
        case .url(let url):
            return url.id
        case .webArchive(let title):
            return title.id
        }
    }
}</pre>
  <p>In <code>ArticlesView</code> we replace states with only one with <code>Sheet</code> value and use one <code>sheet</code> modifier:</p>
  <pre data-lang="swift">struct ArticlesView: View {

    let article: Article = .init(id: .init(),
                                 title: &quot;Sheet happens&quot;,
                                 url: URL(string: &quot;https://blog.artemnovichkov.com/editor/sheet-happens&quot;)!)
    
    @State var sheet: Sheet?
    @State var isOn = true
    
    var body: some View {
        HStack {
            Button(&quot;Show Article&quot;) {
                if isOn {
                    sheet = .url(article.url)
                }
                else {
                    sheet = .webArchive(article.title)
                }
            }
            .sheet(item: $sheet) { sheet in
                switch sheet {
                case .url(let url):
                    SafariView(url: url)
                case .webArchive(let title):
                    WebArchiveView(title: title)
                }
            }
            Toggle(&quot;&quot;, isOn: $isOn)
        }
        .padding()
    }
}</pre>
  <p>Now, with sheet state, both views will be presented 🎉</p>
  <p>If we want to reuse these sheets in another view, we can conform <code>View</code> protocol and return related views in body:</p>
  <pre data-lang="swift">enum Sheet: View, Identifiable {
    
    case url(URL)
    case webArchive(String)
    
    var id: String {
        switch self {
        case .url(let url):
            return url.id
        case .webArchive(let title):
            return title.id
        }
    }
    
    var body: some View {
        switch self {
        case .url(let url):
            SafariView(url: url)
        case .webArchive(let title):
            WebArchiveView(title: title)
        }
    }
}

// in ArticlesView
.sheet(item: $sheet) { $0 }</pre>
  <p>The final code for the example is available <a href="https://github.com/artemnovichkov/sheet" target="_blank">here</a>.</p>
  <h2>Updates for iOS &amp; iPadOS 14.5 Beta 3</h2>
  <p>According to <a href="https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-14_5-beta-release-notes" target="_blank">release notes</a>, we can now apply multiple <code>sheet(isPresented:onDismiss:content:)</code> and <code>fullScreenCover(item:onDismiss:content:)</code> modifiers in the same view hierarchy. (74246633). But if we need to support previous versions (oh, come on, of course we need 😀), we can use the way described above.</p>
  <h2>Conclusion</h2>
  <p>As we can see, SwiftUI has a simple and declarative way for showing sheets with bindings magic. But in real apps with different layouts we must use it wisely and know edge cases of its behavior. Feel free to ping on Twitter, ask your questions related to this article, highlight grammar mistakes , or share your cases. Thanks for reading!</p>
  <tt-tags>
    <tt-tag name="swiftui">#swiftui</tt-tag>
    <tt-tag name="ios">#ios</tt-tag>
    <tt-tag name="swift">#swift</tt-tag>
  </tt-tags>
  <hr />
  <p data-align="center"><a href="https://twitter.com/iosartem" target="_blank">Twitter</a> · <a href="https://t.me/subtlesettings" target="_blank">Telegram</a> · <a href="http://github.com/artemnovichkov" target="_blank">Github</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.artemnovichkov.com/developing-xcode-extensions-tips-and-tricks</guid><link>https://blog.artemnovichkov.com/developing-xcode-extensions-tips-and-tricks?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov</link><comments>https://blog.artemnovichkov.com/developing-xcode-extensions-tips-and-tricks?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov#comments</comments><dc:creator>artemnovichkov</dc:creator><title>Developing Xcode Extensions. Tips and tricks</title><pubDate>Sun, 21 Feb 2021 10:08:59 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/da/be/dabe327b-c388-432f-bec2-b6e7a555a400.png"></media:content><category>iOS</category><tt:hashtag>macos</tt:hashtag><tt:hashtag>swift</tt:hashtag><tt:hashtag>xcode</tt:hashtag><tt:hashtag>xcode_extension</tt:hashtag><tt:hashtag>swiftui</tt:hashtag><description><![CDATA[<img src="https://teletype.in/files/22/cc/22cc5a62-ee19-4ea8-a1e6-8917b33906eb.png"></img>The article is now available on my blog:]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p>The article is now available on my blog:</p>
    <p><a href="https://www.artemnovichkov.com/blog/developing-xcode-extensions-tips-and-tricks" target="_blank">https://www.artemnovichkov.com/blog/developing-xcode-extensions-tips-and-tricks</a></p>
  </section>
  <figure class="m_column">
    <img src="https://teletype.in/files/22/cc/22cc5a62-ee19-4ea8-a1e6-8917b33906eb.png" width="1200" />
  </figure>
  <p>You all see these <a href="https://twitter.com/mecid/status/1359468427396714504?s=20" target="_blank">beautiful</a> <a href="https://twitter.com/twannl/status/1359823904609603584?s=20" target="_blank">code</a> <a href="https://twitter.com/mkj_is/status/1361204092320632836?s=20" target="_blank">screenshots</a> on Twitter. I often use <a href="https://carbon.now.sh" target="_blank">Carbon</a> for my tweets, but Raycast team released <a href="https://ray.so" target="_blank">ray.so</a> that looks prettier and has options for customization via query parameters. I decided to write an Xcode Source Extension for it. Here is my journey, enjoy the reading!</p>
  <h2>The app is first</h2>
  <p>Extensions can be installed only within main apps, and it&#x27;s a good chance to use SwiftUI for macOS development. My app contains options for sharing: colors, background, dark mode, and paddings. Finally, the app looks like this:</p>
  <figure class="m_column">
    <img src="https://teletype.in/files/65/97/65974f8c-c06f-4643-aadd-59ecaa250077.png" width="1224" />
  </figure>
  <p>All selected options are saved to UserDefaults and shared to the extension via app groups. The app is based on SwiftUI App template, and it is very limited for configuration. For instance, it is very hard to disable the fullscreen toolbar button. </p>
  <p>Since macOS 11.0 SDK you can use <a href="https://developer.apple.com/documentation/swiftui/scene/defaultappstorage(_:)" target="_blank">defaultAppStorage(_:)</a> for scenes and views in your apps. All nested @AppStorage property wrappers will use it by default. In theory. But it doesn&#x27;t work for me, so I set all stores explicitly:</p>
  <pre data-lang="swift">private static let defaults = UserDefaults(suiteName: Constants.suiteName)!

@AppStorage(Constants.darkModeKey, store: Self.defaults)
var darkMode: Bool = true</pre>
  <p>And there is no way to check which store is used in @AppStorage. If you know how to fix it, ping me on <a href="http://twitter.com/iosartem" target="_blank">Twitter</a>.</p>
  <h2>Continue with extension</h2>
  <p>It was my first Xcode Source Extension, so I dived into documentation and <a href="https://github.com/theswiftdev/awesome-xcode-extensions" target="_blank">open source extensions</a>. Personally, I think it&#x27;s hard to develop and debug. By the way, here is my final code for getting a selected code:</p>
  <pre data-lang="swift">private func selectedCode(from buffer: XCSourceTextBuffer) -&gt; String {
var text = &quot;&quot;
var spacesCount = 0
for case let range as XCSourceTextRange in buffer.selections {
    for lineNumber in range.start.line...range.end.line {
        if lineNumber &gt;= buffer.lines.count {
            continue
        }
        guard let line = buffer.lines[lineNumber] as? String else {
            continue
        }
        if spacesCount == 0 {
            let currentSpacesCount = line.prefix(while: { $0 == &quot; &quot; }).count
            if currentSpacesCount &gt; 0 {
                spacesCount = currentSpacesCount
            }
        }
        let substring = line.dropFirst(spacesCount)
        text.append(String(substring))
    }
    return text.trimmingCharacters(in: .whitespacesAndNewlines)
}</pre>
  <p>Except extension-specific logic like spacing and trimming, you should work with<code>XCSourceTextBuffer</code>,<code>XCSourceTextRange</code>,<code>XCSourceTextPosition</code> etc.</p>
  <p>When you run your extension, Xcode launches a special instance of Xcode. Sometimes extensions just not show up in Editor menu. Folks on StackOverflow, Apple Forums and Github issues advise to sign extensions with a real certificate, rename Xcode and of course clean/relaunch your projects. Erica Sadun wrote a <a href="https://ericasadun.com/2016/07/21/explorations-into-the-xcode-source-editor-extensions-underbelly-part-1" target="_blank">good article</a> about debugging.</p>
  <p>Finally, when the extension gets selected code, it opens special URL with saved options via<code>NSWorkspace.shared.open</code>.</p>
  <p>To use any installed extensions, select Editor menu in Xcode, and all extensions with commands will appear at the bottom:</p>
  <figure class="m_column">
    <img src="https://teletype.in/files/a7/94/a7941a6c-4fc6-4493-a898-74d3e15dacf9.png" width="2071" />
  </figure>
  <h2>Bonus: Key bindings</h2>
  <p>To be more productive, you can associate a keyboard shortcut with the extension command in <code>Xcode &gt; Preferences... &gt; Key Bindings</code> menu. I use ⌃ + ⌥ + ⌘ + R, because it doesn&#x27;t conflict with default bindings.</p>
  <h2>Results</h2>
  <p>To check the final project, you can open <a href="https://github.com/artemnovichkov/RaySo" target="_blank">RaySo</a> repository and install the app. Regardless all caveats, I&#x27;m happy with the result. Now it&#x27;s so easy to share my code right from Xcode! I just want to add several good articles that help me:</p>
  <ul>
    <li><a href="https://www.vadimbulavin.com/xcode-source-editor-extension-tutorial" target="_blank">Xcode Source Editor Extension Tutorial: Getting Started</a> by <a href="https://twitter.com/V8tr" target="_blank">Vadim Bulavin</a></li>
    <li><a href="https://nshipster.com/xcode-source-extensions" target="_blank">Xcode​Kit and Xcode Source Editor Extensions</a> by <a href="https://twitter.com/zoejessica" target="_blank">Zoë Smith</a></li>
  </ul>
  <tt-tags>
    <tt-tag name="macos">#macos</tt-tag>
    <tt-tag name="swift">#swift</tt-tag>
    <tt-tag name="xcode">#xcode</tt-tag>
    <tt-tag name="xcode_extension">#xcode_extension</tt-tag>
    <tt-tag name="swiftui">#swiftui</tt-tag>
  </tt-tags>
  <hr />
  <p data-align="center"><a href="https://twitter.com/iosartem" target="_blank">Twitter</a> · <a href="https://t.me/subtlesettings" target="_blank">Telegram</a> · <a href="http://github.com/artemnovichkov" target="_blank">Github</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.artemnovichkov.com/custom-environment-value-for-share-actions</guid><link>https://blog.artemnovichkov.com/custom-environment-value-for-share-actions?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov</link><comments>https://blog.artemnovichkov.com/custom-environment-value-for-share-actions?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov#comments</comments><dc:creator>artemnovichkov</dc:creator><title>Custom @Environment value for share actions</title><pubDate>Sat, 13 Feb 2021 11:34:21 GMT</pubDate><category>iOS</category><tt:hashtag>swiftui</tt:hashtag><tt:hashtag>ios</tt:hashtag><tt:hashtag>swift</tt:hashtag><description><![CDATA[<img src="https://teletype.in/files/46/95/46952878-7e94-453e-9730-6466d4058639.png"></img>The article is now available on my blog:]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(323, 50%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p>The article is now available on my blog:</p>
    <p><a href="https://www.artemnovichkov.com/blog/custom-environment-value-for-share-actions" target="_blank">https://www.artemnovichkov.com/blog/custom-environment-value-for-share-actions</a></p>
  </section>
  <figure class="m_original">
    <img src="https://teletype.in/files/46/95/46952878-7e94-453e-9730-6466d4058639.png" width="628" />
  </figure>
  <p>SwiftUI has a lot of modern and useful features. One of my favourite is @Environment property wrapper. It allows you to get system-wide settings, for instance, current locale or color scheme. Since iOS 14.0 you can use<code>openURL</code> value to open URLs from apps easily.</p>
  <pre data-lang="swift">@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension EnvironmentValues {

/// Opens a URL using the appropriate system service.
    public var openURL: OpenURLAction { get }
}</pre>
  <p>With just a one line of code you can extend behaviour of you views:</p>
  <pre data-lang="swift">import SwiftUI

struct ContentView: View {

    @Environment(\.openURL) var openURL
    
    var body: some View {
        Button(&quot;Share&quot;) {
            openURL(URL(string: &quot;https://blog.artemnovichkov.com&quot;)!)
        }
    }
}</pre>
  <p>I was wondering how it works under the hood and tried to implement the same trick for sharing activity items with<code>UIActivityViewController</code>. Let&#x27;s check the result!</p>
  <h2>Keys and values</h2>
  <p>At first we should create a custom key, conform to<code>EnvironmentKey</code> protocol and set a default value. It will be an empty struct for now:</p>
  <pre data-lang="swift">struct ShareAction {}

struct ShareActionEnvironmentKey: EnvironmentKey {

    static let defaultValue: ShareAction = .init()
}</pre>
  <p>Next, we should extend<code>EnvironmentValues</code> to make our<code>ShareAction</code> be available from the environment:</p>
  <pre data-lang="swift">extension EnvironmentValues {

    var share: ShareAction {
        self[ShareActionEnvironmentKey]
    }
}</pre>
  <p>The last thing is adding<code>callAsFunction</code> in<code>ShareAction</code> struct to use the same syntax:</p>
  <pre data-lang="swift">struct ShareAction {

    func callAsFunction(_ activityItems: [Any]) {
        let vc = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
        UIApplication.shared.windows.first?.rootViewController?.present(vc, animated: true, completion: nil)
    }
}</pre>
  <blockquote>Unfortunately, there is no SwiftUI-way to open this controller. If you use a better way, let me know!</blockquote>
  <p>To read more about callable values of user-defined nominal types, check <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0253-callable.md" target="_blank">SE-0253</a> proposal on Github.</p>
  <p>That&#x27;s it, now we can use<code>.share</code> value and share activities from any view:</p>
  <pre data-lang="swift">import SwiftUI

struct ContentView: View {

    @Environment(\.share) var share
    
    var body: some View {
        Button(&quot;Share&quot;) {
            share([URL(string: &quot;https://blog.artemnovichkov.com&quot;)!])
        }
    }
}</pre>
  <h2>Related Resources</h2>
  <p>The final code is available <a href="https://github.com/artemnovichkov/ShareEnvironmentExample" target="_blank">here,</a> and there is a list of related articles:</p>
  <ul>
    <li><a href="https://developer.apple.com/documentation/swiftui/environment" target="_blank">Environment Documentation</a></li>
    <li><a href="https://sarunw.com/posts/what-is-environment-in-swiftui/" target="_blank">What is @Environment in SwiftUI</a> by <a href="https://twitter.com/sarunw" target="_blank">Sarun W.</a></li>
    <li><a href="https://swiftwithmajid.com/2019/08/21/the-power-of-environment-in-swiftui/" target="_blank">The power of Environment in SwiftUI</a> by <a href="https://twitter.com/mecid" target="_blank">Majid Jabrayilov</a></li>
    <li><a href="https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views" target="_blank">How to use @EnvironmentObject to share data between views</a> by <a href="https://twitter.com/twostraws" target="_blank">Paul Hudson</a></li>
  </ul>
  <tt-tags>
    <tt-tag name="swiftui">#swiftui</tt-tag>
    <tt-tag name="ios">#ios</tt-tag>
    <tt-tag name="swift">#swift</tt-tag>
  </tt-tags>
  <hr />
  <p data-align="center"><a href="https://twitter.com/iosartem" target="_blank">Twitter</a> · <a href="https://t.me/subtlesettings" target="_blank">Telegram</a> · <a href="http://github.com/artemnovichkov" target="_blank">Github</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.artemnovichkov.com/ios-courses</guid><link>https://blog.artemnovichkov.com/ios-courses?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov</link><comments>https://blog.artemnovichkov.com/ios-courses?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=artemnovichkov#comments</comments><dc:creator>artemnovichkov</dc:creator><title>iOS-курсы. Дьявол кроется в деталях.</title><pubDate>Mon, 28 Sep 2020 04:21:49 GMT</pubDate><media:content medium="image" url="https://teletype.in/files/49/e4/49e48c61-e3c5-45c0-914c-b8c5b628fbda.png"></media:content><category>iOS</category><tt:hashtag>ios</tt:hashtag><tt:hashtag>разработка</tt:hashtag><tt:hashtag>курсы</tt:hashtag><tt:hashtag>нетология</tt:hashtag><tt:hashtag>skillbox</tt:hashtag><tt:hashtag>skillfactory</tt:hashtag><description><![CDATA[<img src="https://teletype.in/files/83/d2/83d27d7a-0e60-4c6f-863d-b3d9e502d039.png"></img>Я с 2014 года занимаюсь iOS-разработкой. Кроме кода я люблю писать тексты и обращать внимание на детали.]]></description><content:encoded><![CDATA[
  <figure class="m_column">
    <img src="https://teletype.in/files/83/d2/83d27d7a-0e60-4c6f-863d-b3d9e502d039.png" width="1920" />
  </figure>
  <p>Я с 2014 года занимаюсь iOS-разработкой. Кроме кода я люблю писать тексты и обращать внимание на детали.</p>
  <p>В последнее время контекстная реклама показывает мне много образовательных курсов, в том числе по iOS. Очень часто на такие курсы записываются люди из других профессий, далёких от разработки и IT. В программах курсов, с которыми я знакомился, есть ошибки, опечатки и неточности. Неподготовленный человек может пропустить их, но мой натренированный глаз и профессионально деформированный мозг легко подмечают такое.</p>
  <p>Давайте вместе разберём тексты разных программ и чуть-чуть побомбим. <strong>Grammar nazi mode on.</strong></p>
  <blockquote>Я не буду разбирать содержание курсов, полноту и актуальность различных модулей. Это тема для отдельной статьи.</blockquote>
  <h3>Skillbox</h3>
  <p><a href="https://skillbox.ru/course/profession-ios-developer" target="_blank">Ссылка на программу</a></p>
  <figure class="m_column">
    <img src="https://teletype.in/files/68/41/68418dc8-9525-41f3-a953-b6f8302a4cbe.png" width="2160" />
  </figure>
  <blockquote>iOS-разработчик для начинающих</blockquote>
  <blockquote>1. Введение в iOS разработку: переменные и константы</blockquote>
  <p>Так, давайте определимся, нужен дефис после iOS или нет?</p>
  <blockquote>3. Функции и опшиналы</blockquote>
  <p>Как только не обзывали тип <a href="https://developer.apple.com/documentation/swift/optional" target="_blank">Optional</a> — опшионалы, опшиналы, опционалы... А какой вариант на русском вам больше нравится?</p>
  <blockquote>6. xcode.Сontroller и сториборд</blockquote>
  <p>Тут ребята собрали комбо, давайте разбираться:</p>
  <ul>
    <li>У разработчиков часто <a href="https://twitter.com/steipete/status/1260417457803665410" target="_blank">бомбит</a> с ошибок в написании Xcode. Насколько можно доверять курсу, в описании которого название IDE так написано? Этим же грешат эйчары: в описании вакансий часто встречаются подобные ошибки, для меня это лакмусовая бумажка для вакансии. Вижу xCode — помечаю как прочитанное.</li>
    <li>xcode.Сontroller — имели в виду &quot;Xcode, Сontroller&quot;. Но эти ошибки пропустили все люди, через которых проходит этот текст.</li>
  </ul>
  <blockquote>9. Autolayout, Constrains, StackView</blockquote>
  <p>В документации всегда пишут <strong>Auto Layout</strong>. Давайте называть технологии правильно! И опечаток вроде Constrains стоит избегать, правильно — <strong>Constraints</strong>. Или это не опечатка, что ещё хуже.</p>
  <blockquote>16. Подпись и отправка приложений в AppStore. Обзор iTunes connect</blockquote>
  <p>Ещё один хороший показатель того, как часто обновляют программу курса. iTunes connect давно переименовали в App Store Connect. Может, и в лекции показывают старый интерфейс?</p>
  <blockquote>4. Архитектуры приложений: MVC, MVVM, Viper, Amber</blockquote>
  <p>А тут уже интереснее. Первый три архитектуры популярны, они используются в реальных проектах. А вот что такое Amber? Я даже не слышал про неё. Может, я отстал, и каждый джун уже использует её в своих pet-проектах? Я провёл небольшое расследование и нашел <a href="https://github.com/Anvics/Amber" target="_blank">репозиторий</a>: всего 10 звезд, последние обновления были в 2017 году. Автор этой архитектуры — Никита Архипов, он же автор курса от Skillbox. Он даже хотел рассказать про Amber на AppsConf, но доклад почему-то отклонили. Вопрос: стоит ли молодым ребятам изучать её? Вопрос скорее риторический.</p>
  <figure class="m_column">
    <img src="https://teletype.in/files/2e/35/2e3593c2-3dad-4b88-aa98-e7d1bb6a0431.png" width="1544" />
  </figure>
  <blockquote>8. Взаимодействие с Objective С и С-кодом. Секретные фишки из Objective C</blockquote>
  <p>Секретная фишка — правильно писать &quot;Objective-С&quot;.</p>
  <h3>SkillFactory</h3>
  <p><a href="https://skillfactory.ru/iosdev-syllabus-thankyou" target="_blank">Ссылка на программу</a></p>
  <figure class="m_column">
    <img src="https://teletype.in/files/b9/7f/b97ff2b8-90c4-4abb-bdd1-a8737d1928a9.png" width="2160" />
  </figure>
  <blockquote>Структуры и энумы</blockquote>
  <p>А чем &quot;перечисляемый тип&quot; не устроил? Хотя Google умный, Google всё равно подскажет.</p>
  <blockquote>Опшионалы. Строки. Классы</blockquote>
  <p>Давайте уже соберёмся всем сообществом и определимся, как правильно писать.</p>
  <blockquote>Клиент-серверное взаимодейтсвие. Протокол HTTP, RESTful APIs</blockquote>
  <p>Опечатки — они хитрые. Сразу и не заметишь. Жалко, что тут компилятор не может подсказать...</p>
  <blockquote>Unit-текстирование. TDD-тестирование. Фреймворк XCTest</blockquote>
  <p>Насколько я понял, теКстирование — это проверка локализации приложения?</p>
  <blockquote>Работа с Review Guideline и iTunesConnect</blockquote>
  <p>Та же ловушка. Apple, хватит менять сервисы, мы не успеваем!</p>
  <h3>Нетология</h3>
  <p><a href="https://netology.ru/programs/ios-developer" target="_blank">Ссылка на программу</a></p>
  <figure class="m_column">
    <img src="https://teletype.in/files/6b/de/6bde8ada-652c-4494-91d3-464de6145ce6.png" width="2160" />
  </figure>
  <blockquote>познакомитесь с UIView и UIControl, их сабклассами и жизненным циклом UIView</blockquote>
  <p>Обычно говорят про жизненный цикл UIViewController, и на собеседованиях любят спрашивать. А тут и у UIView тоже появился жизненный цикл.</p>
  <blockquote>переход на модальный экран редактирования привычки с использованием стандартных компонентов iOS — UIDatePicker и UITextFieldOperations и Operations Queue: API для создания многопоточного кода</blockquote>
  <p>Нет времени объяснять и ставить пробелы в программе, пошли учиться!</p>
  <blockquote>научитесь проводить Unit- и UI-тесты приложения</blockquote>
  <p>Скорее всего докапываюсь, но обычно &quot;проводят тестирование&quot; или &quot;пишут тесты&quot;.</p>
  <blockquote>Регистрация своего приложение на портале для разработчиков ВКонтакте</blockquote>
  <p>Падежи всем трудно даются.</p>
  <h3>Otus</h3>
  <p><a href="https://otus.ru/lessons/basic-ios/" target="_blank">Ссылка на программу</a></p>
  <figure class="m_column">
    <img src="https://teletype.in/files/40/3e/403e1356-6777-4a2e-b3aa-1b7a2181477a.png" width="2160" />
  </figure>
  <blockquote>XCode, Storyboard, объекты UI, создание программно объектов, XIB</blockquote>
  <p>Про Xcode мы уже поговорили выше. А тут ещё и Yoda style: &quot;создание программно объектов научишься ты!&quot;</p>
  <blockquote>Fabric, Crashlitics</blockquote>
  <p>Ну и что, что сервисы deprecated, legacy кому-то надо поддерживать! А Crashl<strong>y</strong>tics я и сам не всегда с первого раза правильно пишу.</p>
  <h3>Swiftlab</h3>
  <p><a href="https://swiftlab.ru/iOS-developer" target="_blank">Ссылка на программу</a></p>
  <figure class="m_column">
    <img src="https://teletype.in/files/26/f3/26f3b0a3-64ae-4333-ac1b-1e36bab08703.png" width="2160" />
  </figure>
  <blockquote>Панаромирование</blockquote>
  <p>Панарома — интересный термин. А уж как его в разработке можно применить...</p>
  <blockquote>Щипки (pinching gesture)</blockquote>
  <p>Надеюсь, не рассказывают, как щипать коллегу в офисе. И тут Google в очередной раз выручает и показывает результаты про <strong>pinch gesture</strong>.</p>
  <figure class="m_column">
    <img src="https://teletype.in/files/8e/9d/8e9dec72-5830-48e2-b7a0-79f91ae0b754.png" width="3584" />
  </figure>
  <p>Автор курса (как и я при написании этой статьи) устал в конце и в описании модуля про Github просто скопировал описание модуля про Apple Watch. Повторение — мать учения.</p>
  <h3>И что теперь, вообще не учиться?</h3>
  <p>Конечно, нет! Нужно больше вникать в материалы курсов, сравнивать программы и не вестись на маркетинговые уловки.</p>
  <p><strong>Grammar nazi mode off.</strong></p>
  <tt-tags>
    <tt-tag name="ios">#ios</tt-tag>
    <tt-tag name="разработка">#разработка</tt-tag>
    <tt-tag name="курсы">#курсы</tt-tag>
    <tt-tag name="нетология">#нетология</tt-tag>
    <tt-tag name="skillbox">#skillbox</tt-tag>
    <tt-tag name="skillfactory">#skillfactory</tt-tag>
  </tt-tags>
  <hr />
  <p data-align="center"><a href="http://t.me/subtlesetings" target="_blank">Twitter | Telegram</a> | <a href="http://github.com/artemnovichkov" target="_blank">Github</a></p>

]]></content:encoded></item></channel></rss>