<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Oleg Dreyman on Medium]]></title>
        <description><![CDATA[Stories by Oleg Dreyman on Medium]]></description>
        <link>https://medium.com/@olegdreyman?source=rss-7872af0bbed1------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*B4VZypDWh5c6ym7fRUDE4A.png</url>
            <title>Stories by Oleg Dreyman on Medium</title>
            <link>https://medium.com/@olegdreyman?source=rss-7872af0bbed1------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Wed, 16 Sep 2026 10:16:08 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@olegdreyman/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[How to pluralize English text in Swift without using Localizable strings]]></title>
            <link>https://olegdreyman.medium.com/how-to-pluralize-english-text-in-swift-without-using-localizable-strings-dc3e0348f1f3?source=rss-7872af0bbed1------2</link>
            <guid isPermaLink="false">https://medium.com/p/dc3e0348f1f3</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[ios-development]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <dc:creator><![CDATA[Oleg Dreyman]]></dc:creator>
            <pubDate>Tue, 07 Apr 2026 13:56:26 GMT</pubDate>
            <atom:updated>2026-04-07T13:58:56.419Z</atom:updated>
            <content:encoded><![CDATA[<h4>How to easily leverage Swift’s automatic grammar agreement feature</h4><blockquote>TL;DR?<br>Reusable type-safe support for automatic plural forms in English &amp; 7 other languages. Full code snippet at the end of the article</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7JymXkLm1KznphRTHlzE5g.png" /></figure><p>First things first, what do I mean when I say “pluralize”?</p><p>It’s a very common problem in app development. I’m sure you’ve written something like this at some point:</p><pre>let count = 10<br>let articleCountString = &quot;\(count) \(count == 1 ? &quot;article&quot; : &quot;articles&quot;)&quot;</pre><p>It feels quite annoying, not to say incredibly wrong.</p><p>Obviously, the <em>correct</em> way to handle this is to use <a href="https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog#Add-variants-for-strings-that-contain-plurals">Xcode’s string catalogs</a>, which support adding separate forms for different kinds of plurals.</p><figure><img alt="Xcode UI allows to vary items in string catalogs by plural" src="https://cdn-images-1.medium.com/max/1024/0*h87xDjH1KYHXZusM.png" /><figcaption>Xcode’s built-in plural variance in string catalogs</figcaption></figure><p>The UI is even somewhat convenient, and definitely a huge improvement over <a href="https://developer.apple.com/documentation/xcode/localizing-strings-that-contain-plurals">stringsdict files</a> that we had to use in pre-Xcode 15 era.</p><p>But, of course, it still takes you out of writing code and into some Xcode configuration files, and of course you still need to write different plural forms yourself.</p><h3>A better way</h3><p>Just to reiterate, if you want to do everything by the book, <strong>use Xcode string catalogs</strong>. But if you want something quicker, simpler and more convenient, there is a well-hidden Swift feature called <strong>automatic grammar agreement</strong>.</p><p>It almost feels like it shouldn’t work, but it does. Here’s an example from an <a href="https://nilcoalescing.com/blog/HandlePluralsInSwiftUITextViewsWithInflection/">excellent article</a> by <a href="https://twitter.com/natpanferova">Natalia Panferova</a>:</p><pre>Text(&quot;You read ^[\(bookCount) book](inflect: true) this year!&quot;)</pre><p>Believe it or not, in SwiftUI this will actually produce a text of “You read 10 books this year!” or “You read 1 book this year!”, depending on the bookCount</p><p>Now, I’m not gonna go into too much detail about how exactly it works. If you want, here are some great articles:</p><ul><li><a href="https://www.swiftjectivec.com/morphology-in-ios-with-automatic-grammar-agreement/">Morphology in Swift by Jordan Morgan</a></li><li><a href="https://nilcoalescing.com/blog/HandlePluralsInSwiftUITextViewsWithInflection/">Handle plurals in SwiftUI Text views with inflection by Natalia Panferova</a></li></ul><p>The issue with Swift’s inflection is that the easiest way to use it is in SwiftUI Text. And even then, you need to remember an obscure ^[\(count) item](inflect: true) syntax, which compiler knows nothing about. Make a typo or a mistake? Tough luck, now it doesn’t work. Want to use inflection outside of SwiftUI? Not your day.</p><p>Luckily, we can quite easily navigate the labyrinth of Swift’s String-adjacent types to make inflection easy-to-use and error-proof anywhere in our app.</p><p>First, let’s create a reusable function that will write the inflect: true syntax for us:</p><pre>func pluralize(<br>    _ count: Int,<br>    _ text: LocalizedStringResource<br>) -&gt; LocalizedStringResource {<br>    &quot;^[\(count) \(text)](inflect: true)&quot;<br>}</pre><p>To use inflection, we must use these LocalizedStringResource types, which Swift will be able to “unfold” into localized text down the line.</p><p>Next, we create a localization template of type String.LocalizationValue, that we then need to feed into the AttributedString(localized:) initializer:</p><pre>let count = 6<br>let template: String.LocalizationValue = &quot;&quot;&quot;<br>You read \(pluralize(count, &quot;book&quot;)) this year<br>&quot;&quot;&quot;<br>let localized = AttributedString(localized: template)</pre><p>But of course, while AttributedString is useful on its own, mostly in app development we want to deal with plain String instances. Luckily, converting AttributedString to String is possible:</p><pre>let string = String(localized.characters[...])</pre><p>Now we finally get “You read 6 books this year”. If it feels like too many steps, it’s because it is. For such an incredibly useful feature, it sure is quite well-hidden (and also poorly documented).</p><p>And, to make things worse, it is incredibly easy to misuse some of these types and end up with a gibberish like You read LocalizedStringResource(key: “^[%lld %@](inflect: true)”, defaultValue... in your UI instead of the text you need.</p><p>Luckily, we can make things much easier <em>and</em> type-safe so that no one will misuse it.</p><p>First, we will move pluralize into a semi-private type, so that we can “lock” this API for only certain scenarios:</p><pre>struct InflectionBuilder {<br>    fileprivate init() { }<br>    func pluralize(<br>        _ count: Int,<br>        _ text: LocalizedStringResource<br>    ) -&gt; LocalizedStringResource {<br>        &quot;^[\(count) \(text)](inflect: true)&quot;<br>    }<br>}</pre><p>As you see, no one can freely create and use InflectionBuilder on their own. Instead, its usage will be restricted to our function called inflected:</p><pre>extension String {<br>    static func inflected(<br>        _ makeTemplate: (InflectionBuilder) -&gt; String.LocalizationValue<br>    ) -&gt; String {<br>        let builder = InflectionBuilder()<br>        let template = makeTemplate(builder)<br>        let attributed = AttributedString(localized: template)<br>        return String(attributed.characters[...])<br>    }<br>}</pre><p>We encapsulated all the necessary steps behind a type-safe API. Now we can use it like this:</p><pre>let pluralized = String.inflected {<br>    &quot;You read \($0.pluralize(count, &quot;book&quot;)) this year&quot;<br>}</pre><p>It is required to use String interpolation here. <strong>Do not</strong> attempt to “build” a full string by concatenating several strings together with a String.inflected in the middle, because Swift gathers clues from the entire sentence to determine the language and other grammar information.</p><p>More notes:</p><ul><li>You are not limited to just using a single word. If you write “published book” instead of “book”, inflected will auto-convert to “5 published books” as well</li><li>I strongly recommend only using <strong>singular forms of words</strong> when writing pluralize. So always write “book” not “books”.</li><li>As per documentation, in addition to English the automatic grammar agreement feature also supports Spanish, German, French, Italian, Portuguese, Hindi, and Korean, with more languages hopefully coming in future iOS versions.</li></ul><h4><strong>TL;DR: Full code snippet with extended functionality</strong></h4><pre>extension String {<br>    /// Allows automatic pluralization using the `(inflect: true)` attribute. Use `$0.pluralize` with string interpolation to produce inflected strings<br>    ///<br>    /// For reference, see:<br>    /// - [How to pluralize English text in Swift without using Localizable strings - olegdreyman](https://olegdreyman.medium.com/how-to-pluralize-english-text-in-swift-without-using-localizable-strings-dc3e0348f1f3)<br>    /// - [Morphology in Swift - swiftjectivec](https://www.swiftjectivec.com/morphology-in-ios-with-automatic-grammar-agreement/)<br>    /// - [Handle plurals in SwiftUI Text views with inflection - nilcoalescing](https://nilcoalescing.com/blog/HandlePluralsInSwiftUITextViewsWithInflection/)<br>    /// - [Taking advantage of the iOS 15 grammar agreement feature for English strings - stackoverflow](https://stackoverflow.com/a/68156533)<br>    static func inflected(<br>        options: AttributedString.LocalizationOptions = .init(),<br>        _ makeTemplate: (InflectionBuilder) -&gt; String.LocalizationValue<br>    ) -&gt; String {<br>        let builder = InflectionBuilder()<br>        let template = makeTemplate(builder)<br>        let attributed = AttributedString(localized: template, options: options)<br>        return String(attributed.characters[...])<br>    }<br>}<br><br>extension AttributedString {<br>    /// Allows automatic pluralization using the `(inflect: true)` attribute. Use `$0.pluralize` with string interpolation to produce inflected strings<br>    ///<br>    /// For reference, see:<br>    /// - [How to pluralize English text in Swift without using Localizable strings - olegdreyman](https://olegdreyman.medium.com/how-to-pluralize-english-text-in-swift-without-using-localizable-strings-dc3e0348f1f3)<br>    /// - [Morphology in Swift - swiftjectivec](https://www.swiftjectivec.com/morphology-in-ios-with-automatic-grammar-agreement/)<br>    /// - [Handle plurals in SwiftUI Text views with inflection - nilcoalescing](https://nilcoalescing.com/blog/HandlePluralsInSwiftUITextViewsWithInflection/)<br>    /// - [Taking advantage of the iOS 15 grammar agreement feature for English strings - stackoverflow](https://stackoverflow.com/a/68156533)<br>    static func inflected(<br>        options: AttributedString.LocalizationOptions = .init(),<br>        _ makeTemplate: (InflectionBuilder) -&gt; String.LocalizationValue<br>    ) -&gt; AttributedString {<br>        let builder = InflectionBuilder()<br>        let template = makeTemplate(builder)<br>        let attributed = AttributedString(localized: template, options: options)<br>        return attributed<br>    }<br>}<br><br>struct InflectionBuilder {<br>    fileprivate init() { }<br>    <br>    /// Can only be used with string interpolation inside `String.inflected { }` or `AttributedString.inflected { }`. Allows automatic pluralization of text.<br>    ///<br>    /// - Warning: Use a **singular** form of the word. So &quot;value&quot;/&quot;active user&quot;, not &quot;values&quot;/&quot;active users&quot;<br>    ///<br>    /// For reference, see:<br>    /// - [How to pluralize English text in Swift without using Localizable strings - olegdreyman](https://olegdreyman.medium.com/how-to-pluralize-english-text-in-swift-without-using-localizable-strings-dc3e0348f1f3)<br>    /// - [Morphology in Swift - swiftjectivec](https://www.swiftjectivec.com/morphology-in-ios-with-automatic-grammar-agreement/)<br>    /// - [Handle plurals in SwiftUI Text views with inflection - nilcoalescing](https://nilcoalescing.com/blog/HandlePluralsInSwiftUITextViewsWithInflection/)<br>    /// - [Taking advantage of the iOS 15 grammar agreement feature for English strings - stackoverflow](https://stackoverflow.com/a/68156533)<br>    func pluralize(<br>        _ count: Int,<br>        _ text: LocalizedStringResource<br>    ) -&gt; LocalizedStringResource {<br>        &quot;^[\(count) \(text)](inflect: true)&quot;<br>    }<br>}</pre><p>Usage:</p><pre>let activeUserCount = 10<br>let activeUsers: String = .inflected {<br>    &quot;There are \($0.pluralize(activeUserCount, &quot;active user&quot;)) online&quot;<br>} // There are 10 active users online</pre><p><strong>Thanks for reading! If you want to support me, please check out my apps: “</strong><a href="https://askyourself.app/"><strong>Ask Yourself Everyday</strong></a><strong>”, “</strong><a href="https://itunes.apple.com/us/app/the-cleaning-app/id1229632235?at=1010lM6d"><strong>Time and Again</strong></a><strong>” &amp; “</strong><a href="https://apps.apple.com/ua/app/learn-numbers-languages/id6467690725"><strong>Learn Numbers: Foreign Languages</strong></a><strong>”.</strong></p><p><strong>For business inquiries, please reach out to me at </strong><a href="mailto:oleg@dreyman.dev"><strong>oleg@dreyman.dev</strong></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=dc3e0348f1f3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Stop using Timer.publish in your SwiftUI views]]></title>
            <link>https://medium.com/parable-engineering/stop-using-timer-publish-in-your-swiftui-views-498ff270860f?source=rss-7872af0bbed1------2</link>
            <guid isPermaLink="false">https://medium.com/p/498ff270860f</guid>
            <category><![CDATA[swiftui]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[timer]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <dc:creator><![CDATA[Oleg Dreyman]]></dc:creator>
            <pubDate>Tue, 20 May 2025 10:39:12 GMT</pubDate>
            <atom:updated>2025-05-20T10:39:12.097Z</atom:updated>
            <content:encoded><![CDATA[<h4>Create a reusable view modifier instead</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lMR3Wu-8wndhI19QNqG4ew.png" /><figcaption>Usage example for the reusable `.onTimer` modifier</figcaption></figure><p>If you ever try to figure out how to use Foundation’s Timer in SwiftUI, the first thing you’ll see will be something like this:</p><pre>struct CurrentDateView: View {<br>    @State private var currentDate = Date.now<br>    <br>    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()<br><br>    var body: some View {<br>        Text(currentDate.formatted(date: .numeric, time: .standard))<br>            .onReceive(timer) { input in<br>                currentDate = input<br>            }<br>    }<br>} </pre><p>You create a Combine publisher directly via Timer.publish function, and then “listen” to it with the onReceive modifier on your view.</p><p>And while there’s nothing particularly wrong with this (it works exactly as it should), I’d argue there’s a better way to achieve this.</p><p>Two things that are imperfect about this solution, in my opinion, are:</p><ol><li>Because of all the peculiarities of Timer publisher (remembering what run loop modes to use, and remembering the autoconnect() call), the API feels unintuitive and you’ll likely end up googling this snippet every time.</li><li>Having a timer publisher as a free-standing property of your View adds unnecessary clutter and complexity for something that should be quite simple, and feels out of place in declarative SwiftUI code.</li></ol><h4>Solution: reusable TimerModifier</h4><p>A much better solution is to encapsulate this logic in a ViewModifier that you can then easily reuse in any part of your app. The end result will look like this:</p><pre>struct CurrentDateView: View {<br>    @State private var currentDate = Date.now<br><br>    var body: some View {<br>        Text(currentDate.formatted(date: .numeric, time: .standard))<br>            .onTimer(every: 1) { date in<br>                currentDate = date<br>            }<br>    }<br>}</pre><p>Doesn’t it look and feel much cleaner?</p><p>The implementation itself is also trivial, because we’re basically encapsulating our publisher logic within a separate ViewModifier</p><pre>struct TimerModifier: ViewModifier {<br>    @State private var timer: Publishers.Autoconnect&lt;Timer.TimerPublisher&gt;<br>    private let perform: (Date) -&gt; Void<br>    <br>    init(<br>        every interval: TimeInterval,<br>        tolerance: TimeInterval?,<br>        perform: @escaping (Date) -&gt; Void<br>    ) {<br>        self.timer = Timer.publish(every: interval, tolerance: tolerance, on: .main, in: .common)<br>            .autoconnect()<br>        self.perform = perform<br>    }<br>    <br>    func body(content: Content) -&gt; some View {<br>        content<br>            .onReceive(timer) { date in<br>                self.perform(date)<br>            }<br>    }<br>}</pre><pre>extension View {<br>    func onTimer(<br>        every interval: TimeInterval,<br>        tolerance: TimeInterval? = nil,<br>        perform: @escaping (Date) -&gt; Void<br>    ) -&gt; some View {<br>        self.modifier(<br>            TimerModifier(<br>                every: interval,<br>                tolerance: tolerance,<br>                perform: perform<br>            )<br>        )<br>    }<br>}</pre><pre>// usage:<br>Text(currentDate.formatted(date: .numeric, time: .standard))<br>    .onTimer(every: 1, tolerance: 0.05) { date in<br>        currentDate = date<br>    }</pre><p>As you see, we can also easily supply tolerance to our timer with this modifier.</p><p>In my opinion, this is one of the under-appreciated features of the ViewModifier — they can contain their own internal @State and all other SwiftUI observations, and can nicely encapsulate complicated logic.</p><p>Now you can use .onTimer API in any part of your app, without ever needing to look up how to use it, or caring about the intricacies of its Combine API.</p><h4>Bonus 1: Cancellation</h4><p>Cancelling a Timer publisher is even more of a hassle because you need to remember this exciting line:</p><pre>timer.upstream.connect().cancel()</pre><p>To incorporate cancellations into our onTimer modifier, we will use something called a “Trigger value pattern”. It’s used in a bunch of Apple’s own APIs and you can find a great overview of it here: <a href="https://swiftwithmajid.com/2024/04/02/trigger-value-pattern-in-swiftui/">Trigger value pattern in SwiftUI</a>.</p><p>In short, we will add a cancelTrigger property to our TimerModifier, and monitor its change via the onChange modifier, so whenever the cancelTrigger value changes, we will use it to cancel the underlying Timer.</p><pre>struct TimerModifier: ViewModifier {<br>    @State private var timer: Publishers.Autoconnect&lt;Timer.TimerPublisher&gt;<br>    let cancelTrigger: AnyHashable?<br>    private let perform: (Date) -&gt; Void<br>    <br>    init(<br>        every interval: TimeInterval,<br>        tolerance: TimeInterval?,<br>        cancelTrigger: AnyHashable?,<br>        perform: @escaping (Date) -&gt; Void<br>    ) {<br>        self.timer = Timer.publish(every: interval, tolerance: tolerance, on: .main, in: .common)<br>            .autoconnect()<br>        self.cancelTrigger = cancelTrigger<br>        self.perform = perform<br>    }<br>    <br>    func body(content: Content) -&gt; some View {<br>        content<br>            .onReceive(timer) { date in<br>                self.perform(date)<br>            }<br>            .onChange(of: cancelTrigger) {<br>                self.timer.upstream.connect().cancel()<br>            }<br>    }<br>}</pre><pre>extension View {<br>    func onTimer(<br>        every interval: TimeInterval,<br>        tolerance: TimeInterval? = nil,<br>        cancelTrigger: AnyHashable? = nil, // providing a trigger is optional<br>        perform: @escaping (Date) -&gt; Void<br>    ) -&gt; some View {<br>        self.modifier(<br>            TimerModifier(<br>                every: interval,<br>                tolerance: tolerance,<br>                cancelTrigger: cancelTrigger,<br>                perform: perform<br>            )<br>        )<br>    }<br>}</pre><p>And this is how we can cancel a Timer by pressing a button</p><pre>struct CurrentDateView: View {<br>    @State private var currentDate = Date.now<br>    @State private var cancelTimer = false<br><br>    var body: some View {<br>        VStack {<br>            Text(currentDate.formatted(date: .numeric, time: .standard))<br>            Button(&quot;Stop Timer&quot;) {<br>                cancelTimer = true<br>            }<br>        }<br>        .onTimer(every: 1, cancelTrigger: cancelTimer) { date in<br>            currentDate = date<br>        }<br>    }<br>}</pre><p>Of course, the timer is also automatically cancelled when the View is removed from the render tree.</p><h4>Conclusion</h4><p>The main idea behind this article is not just to help you simplify the usage of the Timer in SwiftUI, but to generally suggest using view modifiers to encapsulate complicated logic. We all should strive to make our views as clean and readable as possible, and creating reusable view modifiers are a great way to achieve that.</p><p>A nicely packaged and extended version of this solution is available as a GitHub gist:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b41622570203a7e768c58b5824416bf2/href">https://medium.com/media/b41622570203a7e768c58b5824416bf2/href</a></iframe><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=498ff270860f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/parable-engineering/stop-using-timer-publish-in-your-swiftui-views-498ff270860f">Stop using Timer.publish in your SwiftUI views</a> was originally published in <a href="https://medium.com/parable-engineering">Parable Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to easily create ZIP files in Swift without third-party dependencies]]></title>
            <link>https://medium.com/parable-engineering/how-to-easily-create-zip-files-in-swift-without-third-party-dependencies-a1c36a451ea1?source=rss-7872af0bbed1------2</link>
            <guid isPermaLink="false">https://medium.com/p/a1c36a451ea1</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[swift]]></category>
            <dc:creator><![CDATA[Oleg Dreyman]]></dc:creator>
            <pubDate>Mon, 14 Oct 2024 22:12:17 GMT</pubDate>
            <atom:updated>2024-12-23T13:41:10.035Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qq54BPY0qnIxJIQkYtWuTA.png" /></figure><p>If you’ve ever worked on an iOS app with complex networking, sooner or later you’ll probably need to create ZIP archives from your files. In fact, zipping large files can significantly reduce the size of your HTTP payloads, giving you better networking performance &amp; data usage. It is also arguably the best way to send over multiple files at once, instead of building out a huge multi-part request or sending a separate request for each file.</p><p>For most developers, the first instinct when approaching this problem is to reach for a third-party dependency that does it for you. <a href="https://github.com/marmelroy/Zip"><strong>“Zip” by marmelroy</strong></a> is a great Swift library that does just that. However, if you only need to create ZIP files, not unzip them, using Apple’s system API is enough.</p><p>Here’s how you can do it using Apple’s <a href="https://developer.apple.com/documentation/foundation/nsfilecoordinatorreadingoptions/nsfilecoordinatorreadingforuploading?language=objc"><strong>NSFileCoordinatorReadingForUploading</strong></a><strong> </strong>API:</p><pre>enum CreateZipError: Swift.Error {<br>    case urlNotADirectory(URL)<br>    case failedToCreateZIP(Swift.Error)<br>}<br><br>func createZip(<br>    zipFinalURL: URL,<br>    fromDirectory directoryURL: URL<br>) throws -&gt; URL {<br>    // see URL extension below<br>    guard directoryURL.isDirectory else {<br>        throw CreateZipError.urlNotADirectory(directoryURL)<br>    }<br>    <br>    var fileManagerError: Swift.Error?<br>    var coordinatorError: NSError?<br>    let coordinator = NSFileCoordinator()<br>    coordinator.coordinate(<br>        readingItemAt: directoryURL,<br>        options: .forUploading,<br>        error: &amp;coordinatorError<br>    ) { zipCreatedURL in<br>        do {<br>            // will fail if file already exists at finalURL<br>            // use `replaceItem` instead if you want &quot;overwrite&quot; behavior<br>            try FileManager.default.moveItem(at: zipCreatedURL, to: zipFinalURL)<br>        } catch {<br>            fileManagerError = error<br>        }<br>    }<br>    if let error = coordinatorError ?? fileManagerError {<br>        throw CreateZipError.failedToCreateZIP(error)<br>    }<br>    return zipFinalURL<br>}<br><br>extension URL {<br>    var isDirectory: Bool {<br>       (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true<br>    }<br>}</pre><pre>let mediaDirectoryURL = /* ... */<br><br>let zipURL = try createZip(<br>    zipFinalURL: FileManager.default.temporaryDirectory.appending(path: &quot;new_archive.zip&quot;),<br>    fromDirectory: mediaDirectoryURL<br>)</pre><p>Per Apple’s documentation, the ZIP file will only be created if a provided URL is a directory:</p><blockquote>If the item being read is a directory (such as a document package), then the snapshot is a new file containing the zipped contents of the directory. The URL passed to the accessor block points to the zipped file.</blockquote><p>This is why we’re using guard directoryURL.isDirectory at the top of the function.</p><p>A helper function to easily create zip files in the tmp directory is also a good idea:</p><pre>func createZipAtTmp(<br>    zipFilename: String,<br>    zipExtension: String = &quot;zip&quot;,<br>    fromDirectory directoryURL: URL<br>) throws -&gt; URL {<br>    let finalURL = FileManager.default.temporaryDirectory<br>        .appending(path: zipFilename)<br>        .appendingPathExtension(zipExtension)<br>    return try createZip(<br>        zipFinalURL: finalURL,<br>        fromDirectory: directoryURL<br>    )<br>}<br><br>let mediaDirectoryURL = /* ... */<br><br>let zipURL = try createZipAtTmp(<br>    zipFilename: &quot;new_archive&quot;,<br>    fromDirectory: mediaDirectoryURL<br>)</pre><h3>Bonus: simplifying ZIP directory composition</h3><p>To simplify creating the “directory to be zipped”, at Parable we built an additional layer called FileToZip</p><pre>enum FileToZip {<br>    case data(Data, filename: String)<br>    case existingFile(URL)<br>    case renamedFile(URL, toFilename: String)<br>}<br><br>extension FileToZip {<br>    func prepareInDirectory(directoryURL: URL) throws {<br>        switch self {<br>        case .data(let data, filename: let filename):<br>            let fileURL = directoryURL.appendingPathComponent(filename)<br>            try data.write(to: fileURL)<br>        case .existingFile(let existingFileURL):<br>            let filename = existingFileURL.lastPathComponent<br>            let newFileURL = directoryURL.appendingPathComponent(filename)<br>            try FileManager.default.copyItem(at: existingFileURL, to: newFileURL)<br>        case .renamedFile(let existingFileURL, toFilename: let filename):<br>            let newFileURL = directoryURL.appendingPathComponent(filename)<br>            try FileManager.default.copyItem(at: existingFileURL, to: newFileURL)<br>        }<br>    }<br>}</pre><p>prepareInDirectory will then be used by our zip function to create a “directory to be zipped” for us</p><pre>func createZipAtTmp(<br>    zipFilename: String,<br>    zipExtension: String = &quot;zip&quot;,<br>    filesToZip: [FileToZip]<br>) throws -&gt; URL {<br>    let directoryToZipURL = FileManager.default.temporaryDirectory<br>        .appending(path: UUID().uuidString)<br>        .appending(path: zipFilename)<br>    try FileManager.default.createDirectory(at: directoryToZipURL, withIntermediateDirectories: true, attributes: [:])<br>    for fileToZip in filesToZip {<br>        try fileToZip.prepareInDirectory(directoryURL: directoryToZipURL)<br>    }<br>    return try createZipAtTmp(<br>        zipFilename: zipFilename,<br>        zipExtension: zipExtension,<br>        fromDirectory: directoryToZipURL<br>    )<br>}</pre><pre>let pngImageData: Data = /* ... */<br>let videoURL: URL = /* ... */<br>let dbFileURL: URL = /* ... */<br><br>let zipURL = try createZipAtTmp(<br>    zipFilename: &quot;new_archive&quot;,<br>    filesToZip: [<br>        .data(imageData, filename: &quot;image.png&quot;),<br>        .existingFile(videoURL),<br>        .renamedFile(dbFileURL, toFilename: &quot;local_db.sqlite&quot;)<br>    ]<br>)</pre><p>A few important things to note in there:</p><ol><li>As you see, we’re creating our “directory to be zipped” inside another directory with a UUID name — this is done to avoid possible naming collisions. You can remove this part, if you prefer.</li><li>If at least one “file to zip” can’t be prepared in a directory, the entire operation will fail. This can happen, for example, if two or more files will have the same filename. You might want to handle errors differently there.</li><li>In all of the functions, if the “zip filename” you’re trying to create already exists in the final destination, the operation will fail. If you don’t want that, use “<a href="https://developer.apple.com/documentation/foundation/filemanager/1412432-replaceitem">replaceItem</a>” instead of “moveItem” in the original createZip function</li></ol><h3>Conclusion</h3><p>As you can see, it’s quite straightforward to create ZIP files on iOS using nothing but system APIs. Important to note that for unzipping, however, you will need to use a third-party solution.</p><p>A nicely packaged and extended version of this solution is available as a GitHub gist:</p><p><a href="https://gist.github.com/dreymonde/793a8a7c2ed5443b1594f528bb7c88a7/b1b12b6a7b41ab17d40a2db642b63765441e5e60">ZipService.swift</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a1c36a451ea1" width="1" height="1" alt=""><hr><p><a href="https://medium.com/parable-engineering/how-to-easily-create-zip-files-in-swift-without-third-party-dependencies-a1c36a451ea1">How to easily create ZIP files in Swift without third-party dependencies</a> was originally published in <a href="https://medium.com/parable-engineering">Parable Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to get the original user-installed version of your app with Swift]]></title>
            <link>https://olegdreyman.medium.com/how-to-get-the-original-user-installed-version-of-your-app-with-swift-25a687deacd8?source=rss-7872af0bbed1------2</link>
            <guid isPermaLink="false">https://medium.com/p/25a687deacd8</guid>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[ios-development]]></category>
            <category><![CDATA[swift-programming]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <dc:creator><![CDATA[Oleg Dreyman]]></dc:creator>
            <pubDate>Thu, 11 Jul 2024 11:21:36 GMT</pubDate>
            <atom:updated>2024-07-11T11:21:36.331Z</atom:updated>
            <content:encoded><![CDATA[<h4>Migrating your pricing strategy and don’t want to ruin it for your existing users? Learn how!</h4><p>If you’ve started with a free app in the App Store, and then you want to make some part of it paid, or if you’ve started with a paid app and want to move it to a subscription option with a free trial, the very important thing is to make right by the users who have already installed your app before.</p><p>If you’ve suddenly stopped providing functionality that your users already paid for, or that they were getting before for free, it can badly damage your reputation, not to mention that it’s just a wrong thing to do.</p><p>The solution is to check which was the first version of your app that the user installed previously, and thus if they’re “eligible” to some of the perks that you are now trying to put behind a paywall. Perhaps there are other reasons you might want to know the originally installed version of the app too.</p><p>There is actually a very straightforward way to get exactly that information, and super reliably, including if the user deleted and reinstalled your app multiple times (you can still know the <em>first</em> version they ever installed).</p><p>For that, you just need to take a look at the <strong>App Store Receipt</strong> that’s included in your app’s bundle. <a href="https://www.revenuecat.com/blog/engineering/validating-app-store-receipts/">This article by RevenueCat</a> provides a good overview of it if you’re unfamiliar with the concept.</p><h3>Parsing the App Store Receipt</h3><p>Getting the useful information out of the NSBundle.appStoreReceiptURL is not easy, as the file itself is a PKCS#7 container.</p><p>I won’t go into details about how one would extract the information on their own. It’s not a trivial task (again, the RevenueCat article outlines that). Instead, I’ll direct you to the amazing <a href="https://github.com/IdeasOnCanvas/AppReceiptValidator">AppReceiptValidator</a> Swift library by <a href="https://github.com/IdeasOnCanvas">IdeasOnCanvas</a>.</p><p>After you add the library to your project with SwiftPM, this is the only thing you’ll need to do:</p><pre>import AppReceiptValidator<br><br>func getOriginalBuildNumber() -&gt; String? {<br>    let receiptValidator = AppReceiptValidator()<br>    let appStoreReceipt = try? receiptValidator.parseReceipt(origin: .installedInMainBundle)<br>    return appStoreReceipt?.originalAppVersion<br>}</pre><p>But, if we’re getting the original<strong>AppVersion</strong> from the receipt, why did I call the function getOriginal<strong>BuildNumber </strong>?</p><p>That’s because, according to <a href="https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html">Apple’s documentation</a>:</p><blockquote><strong>Original Application Version<br></strong>This corresponds to the value of CFBundleVersion (in iOS) or CFBundleShortVersionString (in macOS) in the Info.plist file when the purchase was originally made.</blockquote><p>And CFBundleVersion is what you would see as a “build number” in Xcode as opposed to “marketing version”. So, if you’re making an iOS app, “original app version” will actually be “original build number”.</p><p>So, there are two important things to note about this:</p><ol><li>Remember that this number is the <em>build number</em>, not the <em>marketing version</em></li><li>If you’re using Xcode’s “<a href="https://stackoverflow.com/questions/74936052/how-do-i-use-xcodes-new-manage-version-and-build-number-setting">Manage Version and Build Number</a>” feature, you should know that the build number <em>will get reset with each new version</em> (so if you go from let’s say version 1.0 with build number 5 to version 1.1, next build will have build number 1). Because of this, “original build number” actually becomes absolutely useless.</li></ol><p>In that case, you have two options.</p><p>First, you can stop using the “Manage Version and Build Number” feature, and make sure that your build numbers are not getting reset to zero with each new version. Build numbers should be unique and only go in ascending order. Otherwise, you cannot use App Store Receipt to figure out when the user first installed your app.</p><p>If you cannot achieve that, you’ll have to use original_purchase_date, which you can only get by making a HTTP request with the App Store Receipt to <a href="https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/validating_receipts_with_the_app_store">Apple’s </a><a href="https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/validating_receipts_with_the_app_store">verifyReceipt endpoint</a>. However, as that endpoint is now deprecated, perhaps using consecutive build numbers is the only way forward.</p><p>Sadly, using receipt_creation_date from the local App Store Receipt is not a solution either, as that date <a href="https://forums.developer.apple.com/forums/thread/99325">is reset when the app is deleted and reinstalled</a>.</p><h3>Comparing build numbers</h3><p>So, the getOriginalBuildNumber() function will return a build number.</p><p>Keep in mind that for TestFlight builds, the returned value will always be nil, and for Debug builds — it’s either gonna be “1.0” or also nil (I’ve seen both).</p><p>Now, comparing any kind of version strings (including build numbers) is easy:</p><pre>func isBuildNumber(_ lhs: String, earlierThanBuildNumber rhs: String) -&gt; Bool {<br>    rhs.compare(lhs, options: .numeric) == .orderedDescending<br>}<br><br>let newPricingBuildNumber = &quot;15&quot;<br>let originalBuildNumber = getOriginalBuildNumber()<br><br>if let originalBuildNumber, isBuildNumber(originalBuildNumber, earlierThanBuildNumber: newPricingBuildNumber) {<br>    // user installed the app before build number 15<br>} else {<br>    // user installed the app after build number 15<br>}</pre><p><strong>Thanks for reading! If you want to support me, please check out my apps: “</strong><a href="https://askyourself.app"><strong>Ask Yourself Everyday</strong></a><strong>”, “</strong><a href="https://itunes.apple.com/us/app/the-cleaning-app/id1229632235?at=1010lM6d"><strong>Time and Again</strong></a><strong>” &amp; “</strong><a href="https://apps.apple.com/ua/app/learn-numbers-languages/id6467690725"><strong>Learn Numbers: Foreign Languages</strong></a><strong>”. For business inquiries, please reach out to me at </strong><a href="mailto:oleg@dreyman.dev"><strong>oleg@dreyman.dev</strong></a><strong>. Thanks for reading!</strong></p><p><a href="https://u24.gov.ua">🇺🇦 Donate to Ukraine here</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=25a687deacd8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Simplifying Swift Timers: solving memory leaks & complexity once and for all]]></title>
            <link>https://olegdreyman.medium.com/simplifying-swift-timers-solving-memory-leaks-complexity-once-and-for-all-1fecfeba4f29?source=rss-7872af0bbed1------2</link>
            <guid isPermaLink="false">https://medium.com/p/1fecfeba4f29</guid>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[uikit]]></category>
            <category><![CDATA[timer]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <dc:creator><![CDATA[Oleg Dreyman]]></dc:creator>
            <pubDate>Mon, 17 Jul 2023 21:18:21 GMT</pubDate>
            <atom:updated>2023-07-19T12:05:26.847Z</atom:updated>
            <content:encoded><![CDATA[<h4>Creating easy-to-use Timers with automatic memory management</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q3dh3Y4B6nuvEBSGYL_Ieg.png" /></figure><blockquote><strong>TL;DR?</strong> Explaining how to create a system for intuitive Swift Timers with automatic memory management. Minimal gist <a href="https://gist.github.com/dreymonde/7538c4ac708bafc6d2e743a14ceedd4a">here</a>, fully featured Swift package <a href="https://github.com/dreymonde/Timers">here</a>, or read the article for details</blockquote><blockquote>This article is not an explainer on <strong>Timer</strong> API. If you need a deep dive on Foundation.Timer in Swift, I recommend <a href="https://www.hackingwithswift.com/articles/117/the-ultimate-guide-to-timer">The ultimate guide to Timer</a> by <a href="https://medium.com/u/76b0105a5cfb">Paul Hudson</a></blockquote><p>Swift’s Timer (ex-NSTimer) is a very powerful API, but can be quite unintuitive to use. My main issues with it are:</p><ol><li><strong>Vagueness about memory management: </strong>there are many ways to create and manage Timer instances, and all of them have different rules about when and how they are deallocated. Developers must always stay vigilant, as this complexity can lead to memory leaks and other bugs.</li><li><strong>API complexity:</strong> after figuring out memory management, you are still presented with many options to get right: use Timer.init or Timer.scheduledTimer? Which RunLoop to use? What on earth are run loop modes? Developers often need to go searching for documentation instead of relying on the API intuitively. For the vast majority of use cases, this is simply unnecessary.</li></ol><h3>Solution: “Timers“ class</h3><p>The initial idea is to create a Timers class that will hold references to timers and invalidate them (thus deallocating) once this object itself is deallocated. Here’s the core structure of it:</p><pre>public final class Timers {<br>    <br>    private var timers: [Foundation.Timer] = []<br>    <br>    public init() { }<br>    <br>    public func clear() {<br>        for timer in timers {<br>            timer.invalidate()<br>        }<br>        timers = []<br>    }<br>    <br>    deinit {<br>        clear()<br>    }<br><br>    // to be continued...<br>}</pre><p>Hopefully, the idea is clear. Whoever “owns” the Timers instance (for example, a view controller) controls when the timers are stopped and deallocated. No need to manually juggle timer instances: when the owner gets deallocated, Timers gets deallocated, which invalidates and deallocates all the timers. In the end, the usage will look like this:</p><pre>final class ExampleViewController: UIViewController {<br>    <br>    // will invalidate &amp; deallocate all the timers<br>    // when ExampleViewController deallocates<br>    let timers = Timers()<br>    <br>    override func viewDidLoad() {<br>        super.viewDidLoad()<br>        <br>        timers.addTimer(/*...*/) // we&#39;ll get into specifics next<br>    }<br>}</pre><h4>Adding helper Timer functions</h4><p>Our goal is to create an easy-to-use, intuitive API for Timers while also giving our API users full power of Foundation’s Timer. So first we’ll create one “core” addTimer function that will enable us to make memory-safe timers easily, and then we can add convenience functions that are more than enough for common use cases.</p><p>The “core” function looks like this:</p><pre> extension Timers {<br>    public func addTimerManually(<br>        runLoop: RunLoop = .main,<br>        runLoopMode: RunLoop.Mode = .common,<br>        timer: Foundation.Timer<br>    ) {<br>        runLoop.add(timer, forMode: runLoopMode)<br>        timers.append(timer)<br>    }<br>}</pre><p>Users can still use custom run loop and run loop modes for those more sophisticated scenarios, but this function provides reasonable default values (RunLoop.main and RunLoop.Mode.common) for standard use cases.</p><p>This already gives us the benefit of simplified automatic memory management, since the timer added this way will get automatically invalidated and deallocated when the Timers instance gets deallocated.</p><p>And now it’s time to introduce out first helper function, which goes one step further:</p><pre>extension Timers {<br>    public func addRepeating&lt;TargetType: AnyObject&gt;(<br>        timeInterval: TimeInterval,<br>        tolerance: TimeInterval = 0,<br>        withTarget target: TargetType,<br>        _ handler: @escaping (TargetType, Timer) -&gt; Void<br>    ) {<br>        let newTimer = Timer(timeInterval: timeInterval, repeats: true) { [weak target] timer in<br>            if let target {<br>                handler(target, timer)<br>            }<br>        }<br>        newTimer.tolerance = tolerance<br>        self.addTimerManually(timer: newTimer)<br>    }<br>}</pre><p>We’re using TargetType pattern here to avoid relying on our users typing [weak self] every time to avoid retain cycles. Instead, we are the one who are writing [weak target]. This might look strange to you, but it’s actually a pattern that is <a href="https://developer.apple.com/documentation/foundation/undomanager/2427208-registerundo">used by Apple itself</a>. You can read more about the core idea of this pattern in my article <a href="https://olegdreyman.medium.com/no-more-weak-self-or-the-weird-new-future-of-delegation-f2a2745cd73">No more [weak self], or the weird new future of delegation</a>.</p><p>As such, creating a repeating timer now becomes easy and safe:</p><pre>final class ExampleViewController: UIViewController {<br>    <br>    let timers = Timers()<br>    <br>    override func viewDidLoad() {<br>        super.viewDidLoad()<br>        <br>        timers.addRepeating(timeInterval: 1, withTarget: self) { (self, timer) in<br>            self.reloadData()<br>        }<br>    }<br><br>    // ...<br>}</pre><h4>Adding more helper functions</h4><p>From here, the Timers class is infinitely extendable.</p><p>First, we can write a version of addRepeating where you can simply use (self) instead of (self, timer):</p><pre>extension Timers {<br>    public func addRepeating&lt;TargetType: AnyObject&gt;(<br>        timeInterval: TimeInterval,<br>        tolerance: TimeInterval = 0,<br>        withTarget target: TargetType,<br>        handler: @escaping (TargetType) -&gt; Void<br>    ) {<br>        self.addRepeating(<br>            timeInterval: timeInterval,<br>            tolerance: tolerance,<br>            withTarget: target,<br>            handler: { target, _ in handler(target) }<br>        )<br>    }<br>}</pre><pre>final class ExampleViewController: UIViewController {<br>    <br>    let timers = Timers()<br>    <br>    override func viewDidLoad() {<br>        super.viewDidLoad()<br>        <br>        timers.addRepeating(timeInterval: 1, withTarget: self) { (self) in<br>            self.reloadData()<br>        }<br>    }<br><br>    // ...<br>}</pre><p>This is optional but nice to have, as usually we don’t need direct access to Timer instances in the block.</p><p>Then, we can write additional helpers to support other use cases, for example, this one that fires at a specific time and then repeats with a set interval:</p><pre>extension Timers {<br>    public func addRepeating&lt;TargetType: AnyObject&gt;(<br>        initiallyFireAt fireAt: Date,<br>        thenRepeatWithInterval timeInterval: TimeInterval,<br>        tolerance: TimeInterval = 0,<br>        withTarget target: TargetType,<br>        handler: @escaping (TargetType, Timer) -&gt; Void<br>    ) {<br>        let newTimer = Timer(<br>            fire: fireAt,<br>            interval: timeInterval,<br>            repeats: true,<br>            block: { [weak target] timer in<br>                if let target {<br>                    handler(target, timer)<br>                }<br>            }<br>        )<br>        newTimer.tolerance = tolerance<br>        self.addTimerManually(timer: newTimer)<br>    }<br>}<br><br>// usage:<br>timers.addRepeating(<br>    initiallyFireAt: .now.addingTimeInterval(10),<br>    thenRepeatWithInterval: 5,<br>    withTarget: self<br>) { (self, timer) in<br>    self.reloadData()<br>}</pre><p>Or a timer that fires once:</p><pre>extension Timers {<br>    public func fireAt&lt;TargetType: AnyObject&gt;(<br>        _ fireAt: Date,<br>        withTarget target: TargetType,<br>        handler: @escaping (TargetType) -&gt; Void<br>    ) {<br>        let newTimer = Timer(<br>            fire: fireAt,<br>            interval: 0,<br>            repeats: false,<br>            block: { [weak target] _ in<br>                if let target {<br>                    handler(target)<br>                }<br>            }<br>        )<br>        addTimerManually(timer: newTimer)<br>    }<br>}<br><br>// usage:<br>timers.fireAt(date, withTarget: self) { (self) in<br>    self.reloadData()<br>}</pre><p>It’s straightforward to add your own helper functions to suit your specific needs — even the more complex ones — without sacrificing simplicity and memory-leak safety. Feel free to do so</p><h3>Final code</h3><p>For easy copy-pasting, here’s our final Timers class with one helper function:</p><pre>public final class Timers {<br>    <br>    private var timers: [Foundation.Timer] = []<br>    <br>    public init() { }<br>    <br>    public func clear() {<br>        for timer in timers {<br>            timer.invalidate()<br>        }<br>        timers = []<br>    }<br>    <br>    deinit {<br>        clear()<br>    }<br><br>    public func addTimerManually(<br>        runLoop: RunLoop = .main,<br>        runLoopMode: RunLoop.Mode = .common,<br>        timer: Timer<br>    ) {<br>        runLoop.add(timer, forMode: runLoopMode)<br>        timers.append(timer)<br>    }<br><br>    public func addRepeating&lt;TargetType: AnyObject&gt;(<br>        timeInterval: TimeInterval,<br>        tolerance: TimeInterval = 0,<br>        withTarget target: TargetType,<br>        handler: @escaping (TargetType, Timer) -&gt; Void<br>    ) {<br>        let newTimer = Timer(timeInterval: timeInterval, repeats: true) { [weak target] timer in<br>            if let target {<br>                handler(target, timer)<br>            }<br>        }<br>        newTimer.tolerance = tolerance<br>        addTimerManually(timer: newTimer)<br>    }<br>}</pre><p>An extended version is also available as a Swift package:</p><p><a href="https://github.com/dreymonde/Timers">GitHub - dreymonde/Timers: ⏲️ Intuitive Swift timers with automatic memory management</a></p><p>It has more helper functions and an additional generic function to reduce code repetition and help you write your own helpers more easily.</p><p>Hopefully, this proves helpful. In my personal experience, I have found it much more enjoyable to use the Timers class instead of creating NSTimers directly. It also helps knowing my code is much safer as a result.</p><p><em>Thanks for reading the post! Don’t hesitate to ask or suggest anything in the “responses” section below. You can also contact me on </em><a href="https://twitter.com/olegdreyman"><em>Twitter</em></a><em> or find me on </em><a href="https://github.com/dreymonde"><em>GitHub</em></a><em>. If you’ve written an article — or stumbled upon one — exploring a similar topic, be sure to post a link to it in the responses, and I’ll include it below.</em></p><p><strong>Hi! If you want to support me, please check out my apps: “</strong><a href="https://askyourself.app"><strong>Ask Yourself Everyday</strong></a><strong>” and “</strong><a href="https://itunes.apple.com/us/app/the-cleaning-app/id1229632235?at=1010lM6d"><strong>Time and Again</strong></a><strong>”. For business inquiries, please reach out to me at </strong><a href="mailto:oleg@dreyman.dev"><strong>oleg@dreyman.dev</strong></a><strong>. Thanks for reading!</strong></p><p><a href="https://u24.gov.ua">🇺🇦 Donate to Ukraine here</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1fecfeba4f29" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a reusable system for complex URL requests with Swift]]></title>
            <link>https://medium.com/parable-engineering/building-a-reusable-system-for-complex-url-requests-with-swift-b385ba5fafe2?source=rss-7872af0bbed1------2</link>
            <guid isPermaLink="false">https://medium.com/p/b385ba5fafe2</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[apple]]></category>
            <category><![CDATA[network]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <dc:creator><![CDATA[Oleg Dreyman]]></dc:creator>
            <pubDate>Wed, 20 Jul 2022 18:31:38 GMT</pubDate>
            <atom:updated>2026-04-30T09:13:07.431Z</atom:updated>
            <content:encoded><![CDATA[<h4>Deal with query items, HTTP headers, request body and more in an easy, declarative way</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DuZ2yjZdDzeQBR40QaLW3Q.png" /></figure><p>Theoretically, building a URLRequest in Swift is easy:</p><pre>let googleURL = URL(string: &quot;https://google.com&quot;)!<br>var urlRequest = URLRequest(url: googleURL)<br>urlRequest.httpMethod = &quot;POST&quot;</pre><p>But at some point, when your requests are complex enough, it starts to get frustrating.</p><p>First of all, there will be a lot of repetition — setting all the correct headers (but only in correct places), handling the encoding of the content (if necessary) and so on.</p><p>Secondly, there is also an issue that’s incredibly easy for developers to mess up: a <strong>URL query</strong>.</p><p>Most developers just try to work with the query directly, but percent encoding separate query items can be a tricky thing. Foundation has a built-in mechanism for it — URLComponents with its queryItems property, but it can feel like a hassle to dance around with URLComponents instances.</p><p>Instead, it would’ve been nice if we could operate with a query in a similar way we operate with, say, HTTP headers.</p><p>So in this article we will build a reusable, flexible URL request builder system that will help you out tremendously once you have complex enough requests.</p><h3>Step 1: Separating the path from the base</h3><p>Once you start building your networking layer, you’ll quickly notice that you need a system that allows you to use any base URL (for example, representing different servers based on dev environment), not just the one that’s “hardcoded” into the URL.</p><p>So we’ll start our system by working on a “path” level, like this:</p><pre>struct RequestBuilder {<br>    var urlComponents: URLComponents<br>    <br>    private init(urlComponents: URLComponents) {<br>        self.urlComponents = urlComponents<br>    }<br>    <br>    <strong>init(path: String)</strong> {<br>        var components = URLComponents()<br>        <strong>components.path = path</strong><br>        self.init(urlComponents: components)<br>    }<br>}</pre><p>And then one can build a full URLRequest like this:</p><pre>extension RequestBuilder {<br>    func <strong>makeRequest</strong>(<strong>baseURL</strong>: URL) -&gt; URLRequest? {<br>        guard let finalURL = <strong>urlComponents.url(relativeTo: baseURL)</strong> else {<br>            return nil<br>        }<br>        return URLRequest(url: finalURL)<br>    }<br>}</pre><pre>//</pre><pre>let request = RequestBuilder(<strong>endpoint</strong>: &quot;users/get&quot;)<br>    .<strong>makeRequest</strong>(baseURL: apiBaseURL)</pre><p>relativeTo: is a rather unknown feature of Foundation’s URL ecosystem, but one that works here really well.</p><blockquote>CAUTION: if you get weird errors, make sure that your <strong>base URL</strong> <strong>does not have a “/” at the end</strong>, and that your <strong>path</strong> <strong>does not contain “/” at the start</strong>.</blockquote><h3>Step 2: Adding support for query items</h3><p>Let’s continue by introducing a simple operator “template” for our new features:</p><pre>extension RequestBuilder {<br>    func <strong>modifyURLComponents</strong>(_ modifyURLComponents: @escaping (<strong>inout</strong> URLComponents) -&gt; Void) -&gt; RequestBuilder {<br>        var copy = self<br>        modifyURLComponents(&amp;copy.urlComponents)<br>        return copy<br>    }<br>}</pre><p>This function will modify the stored URLComponents and will return a new instance.</p><p>With this template ready, building the query items support is trivial:</p><pre>extension RequestBuilder {<br>    func queryItems(_ <strong>queryItems: [URLQueryItem]</strong>) -&gt; RequestBuilder {<br>        modifyURLComponents { urlComponents in<br>            var items = urlComponents.queryItems ?? []<br>            items.append(contentsOf: queryItems)<br>            urlComponents.queryItems = items<br>        }<br>    }<br>}</pre><p>From a usage perspective, this will work similarly to how you operate on SwiftUI views or Combine pipelines. And the final use of the whole thing so far is clear:</p><pre>let request = RequestBuilder(path: &quot;users/search&quot;)<br>    .<strong>queryItems</strong>([<br>        URLQueryItem(name: &quot;city&quot;, value: &quot;San Francisco&quot;)<br>    ])<br>    .makeRequest(baseURL: apiBaseURL)</pre><pre><em>// https://example.com/users/search?city=San%20Francisco</em></pre><p>URLQueryItem API provided by Foundation <strong>will perform all the necessary percent encoding for us</strong>, so there’s no need for us to worry about that at all.</p><p>Now we can even add some sugar to make working with the query even nicer:</p><pre>extension RequestBuilder {<br>    func <strong>queryItems</strong>(_ queryItems: [(name: String, value: String)]) -&gt; RequestBuilder {<br>        self.queryItems(queryItems.map { .init(name: $0.name, value: $0.value) })<br>    }<br><br>    func <strong>queryItem</strong>(name: String, value: String) -&gt; RequestBuilder {<br>        queryItems([(name: name, value: value)])<br>    }<br>}</pre><pre>let request = RequestBuilder(path: &quot;users/search&quot;)<br>    .<strong>queryItem</strong>(name: &quot;city&quot;, value: &quot;San Francisco&quot;)<br>    .<strong>queryItem</strong>(name: &quot;maxResults&quot;, value: 100.description)<br>    .makeRequest(baseURL: apiBaseURL)</pre><pre><em>// https://example.com/users/search?city=San%20Francisco&amp;maxResults=100</em></pre><h3>Step 3: Modifying the URL request</h3><p>Well, so far our system works with creating the URL we need, including the query. But what about all the HTTP request stuff?</p><p>Well, the main challenge is that URLRequest and URLComponents both contain a representation of a URL. If we have both the urlRequest and urlComponents in our RequestBuilder, they could end up having “conflicting views” on what the correct URL is.</p><p>Since our RequestBuilder operates on a path level, urlComponents should be the main “source of truth” regarding all things URL. But the issue is — <em>we can’t create a </em><em>URLRequest without a </em><em>URL</em>. So instead, let’s not store the URLRequest instance itself, but instead store <em>instructions</em> on how we’ll need to <em>modify</em> the request <em>after</em> it’s set up with the correct URL.</p><p>It might sound a bit confusing, but the concept is actually quite simple:</p><pre>struct RequestBuilder {<br>    var <strong>buildURLRequest</strong>: (inout URLRequest) -&gt; Void<br>    var urlComponents: URLComponents<br><br>    private init(urlComponents: URLComponents) {<br>        self.urlComponents = urlComponents<br>        self.buildURLRequest = { _ in }<br>    }<br><br>    init(path: String) {<br>        var components = URLComponents()<br>        components.path = path<br>        self.init(urlComponents: components)<br>    }<br><br>    func makeRequest(baseURL: URL) -&gt; URLRequest? {<br>        let finalURL = urlComponents.url(relativeTo: baseURL) ?? baseURL<br><br>        var urlRequest = URLRequest(url: finalURL)<br>        <strong>buildURLRequest(&amp;urlRequest)<br></strong><br>        return urlRequest<br>    }<br>}</pre><p>And the “template” operator is implemented like this:</p><pre>extension RequestBuilder {<br>    func <strong>modifyURLRequest</strong>(_ modifyURLRequest: @escaping (inout URLRequest) -&gt; Void) -&gt; RequestBuilder {<br>        var copy = self<br>        let existing = buildURLRequest<br>        copy.buildURLRequest = { request in<br>            <strong>existing(&amp;request)<br>            modifyURLRequest(&amp;request)</strong><br>        }<br>        return copy<br>    }<br>}</pre><pre>//</pre><pre>let request = RequestBuilder(path: &quot;users/submit&quot;)<br>    .<strong>modifyURLRequest</strong>({ request in<br>        request.httpMethod = &quot;POST&quot;<br>    })<br>    .makeRequest(baseURL: apiBaseURL)</pre><p>So far, we already have a system that:</p><ul><li>Separates the path from the base URL</li><li>Allows working on a URLComponents level, which gives easy control of the query</li><li>Allows us to modify the URLRequest instance</li></ul><h3>Step 4: Creating operators for URL request</h3><p>Now we can add an infinite number of useful operators that modify our URLRequest instance.</p><p>For example, <strong>HTTP method</strong>:</p><pre>extension RequestBuilder {<br>    enum HTTPRequestMethod: String {<br>        case get = &quot;GET&quot;<br>        case post = &quot;POST&quot;<br>        case put = &quot;PUT&quot;<br>        case head = &quot;HEAD&quot;<br>        case delete = &quot;DELETE&quot;<br>        case patch = &quot;PATCH&quot;<br>        case options = &quot;OPTIONS&quot;<br>        case connect = &quot;CONNECT&quot;<br>        case trace = &quot;TRACE&quot;<br>    }<br><br>    func <strong>httpMethod</strong>(_ method: HTTPRequestMethod) -&gt; RequestBuilder {<br>        modifyURLRequest { $0.<strong>httpMethod</strong> = method.rawValue }<br>    }<br>}</pre><pre>//</pre><pre>let request = RequestBuilder(path: &quot;users/submit&quot;)<br>    .<strong>httpMethod</strong>(.post)<br>    .makeRequest(baseURL: apiBaseURL)</pre><p>Or <strong>HTTP headers</strong>:</p><pre>extension RequestBuilder {<br>    func <strong>httpHeader</strong>(name: String, value: String) -&gt; RequestBuilder {<br>        modifyURLRequest { $0.<strong>addValue</strong>(value, <strong>forHTTPHeaderField</strong>: name) }<br>    }<br>}</pre><pre>//</pre><pre>let request = RequestBuilder(path: &quot;users/submit&quot;)<br>    .httpMethod(.post)<br>    .httpHeader(name: &quot;Content-Type&quot;, value: &quot;application/json&quot;)<br>    .makeRequest(baseURL: apiBaseURL)</pre><p><strong>HTTP body:</strong></p><pre>extension RequestBuilder {<br>    func <strong>httpBody</strong>(_ body: Data) -&gt; RequestBuilder {<br>        modifyURLRequest { $0.httpBody = body }<br>    }<br><br>    static let jsonEncoder = JSONEncoder()<br><br>    func <strong>httpJSONBody</strong>&lt;Content: Encodable&gt;(_ body: Content, encoder: JSONEncoder = RequestBuilder.jsonEncoder) throws -&gt; RequestBuilder {<br>        let body = try encoder.encode(body)<br>        return httpBody(body)<br>    }<br>}</pre><pre>//</pre><pre>let request = RequestBuilder(path: &quot;users/submit&quot;)<br>    .httpMethod(.post)<br>    .httpHeader(name: &quot;Content-Type&quot;, value: &quot;application/json&quot;)<br>    .<strong>httpJSONBody</strong>(userForm)<br>    .makeRequest(baseURL: apiBaseURL)</pre><p><strong>Timeout</strong>:</p><pre>extension RequestBuilder {<br>    func <strong>timeout</strong>(seconds timeout: TimeInterval) -&gt; RequestBuilder {<br>        modifyURLRequest { $0.<strong>timeoutInterval</strong> = timeout }<br>    }<br>}</pre><pre>let request = RequestBuilder(path: &quot;users/submit&quot;)<br>    .httpMethod(.post)<br>    .httpHeader(name: &quot;Content-Type&quot;, value: &quot;application/json&quot;)<br>    .<strong>timeout(seconds</strong>: 10)<br>    .makeRequest(baseURL: apiBaseURL)</pre><p>As you can see, we can have as many of these operators as we want. The system is really flexible, and you can always create operators that are specific to your own app.</p><p>What we also found helpful is the concept of “<strong>factories</strong>” — easy “starting points” for your requests. For example, these are the one we use often:</p><pre>extension RequestBuilder {<br>    // MARK: - Factories<br><br>    static func <strong>get</strong>(path: String) -&gt; RequestBuilder {<br>        RequestBuilder(path: path)<br>            .httpMethod(.<strong>get</strong>)<br>    }<br><br>    static func <strong>post</strong>(path: String) -&gt; RequestBuilder {<br>        RequestBuilder(path: path)<br>            .httpMethod(.<strong>post</strong>)<br>    }<br><br>    // MARK: - JSON Factories<br><br>    static func <strong>jsonGet</strong>(path: String) -&gt; RequestBuilder {<br>        .get(path: path)<br>            .httpHeader(name: &quot;Accept&quot;, value: &quot;application/json&quot;)<br>    }<br><br>    static func <strong>jsonPost</strong>(path: String, jsonData: Data) -&gt; RequestBuilder {<br>        .post(path: path)<br>            .httpHeader(name: &quot;Content-Type&quot;, value: &quot;application/json&quot;)<br>            .httpBody(jsonData)<br>    }<br><br>    static func <strong>jsonPost</strong>&lt;Content: Encodable&gt;(<br>        path: String,<br>        jsonObject: Content,<br>        encoder: JSONEncoder = RequestBuilder.jsonEncoder<br>    ) throws -&gt; EndpointRequest {<br>        try .post(path: path)<br>            .httpHeader(name: &quot;Content-Type&quot;, value: &quot;application/json&quot;)<br>            .<strong>httpJSONBody</strong>(jsonObject, encoder: encoder)<br>    }<br>}<br><br>//<br><br>let request = RequestBuilder.<strong>jsonPost(path: &quot;users/submit&quot;, jsonObject: userForm)</strong><br>    .timeout(seconds: 10)<br>    .makeRequest(baseURL: apiBaseURL)</pre><p>The system can really be extended and adapted to your every need.</p><h3>Conclusion</h3><p>Hopefully, you find this guide useful. For us, the biggest benefit of this approach is that we can operate on query items and HTTP stuff in the same centralised space, without messing with URLComponents directly. It simplified a lot of the code we had — code was not only bulky and sometimes hard to grasp, but also very repetitive and error-prone.</p><p>Of course, there are still many improvements that can be made here. Let us know in the comments if there’s anything you’d want to add to this system!</p><p>We also published an improved version of this system to our GitHub account, feel free to check it out here:</p><p><a href="https://github.com/ParableHealth/URLRequestBuilder">GitHub - ParableHealth/URLRequestBuilder: Reusable system for complex URL requests with Swift. Deal with query items, HTTP headers, request body and more in an easy, declarative way</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b385ba5fafe2" width="1" height="1" alt=""><hr><p><a href="https://medium.com/parable-engineering/building-a-reusable-system-for-complex-url-requests-with-swift-b385ba5fafe2">Building a reusable system for complex URL requests with Swift</a> was originally published in <a href="https://medium.com/parable-engineering">Parable Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Animator: easy trick to make UIKit animations reusable]]></title>
            <link>https://olegdreyman.medium.com/animator-easy-trick-to-make-uikit-animations-reusable-2d10713ca3a?source=rss-7872af0bbed1------2</link>
            <guid isPermaLink="false">https://medium.com/p/2d10713ca3a</guid>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[uikit]]></category>
            <category><![CDATA[animation]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[articles]]></category>
            <dc:creator><![CDATA[Oleg Dreyman]]></dc:creator>
            <pubDate>Sun, 14 Nov 2021 15:54:20 GMT</pubDate>
            <atom:updated>2024-08-05T02:52:51.184Z</atom:updated>
            <content:encoded><![CDATA[<h4>Fighting duplicate animation code in two easy steps</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Cb8p_dDNo9PZRQLhDlsSYA.png" /></figure><h4>The Problem</h4><p>Let’s say after years of experimentation, you finally came up with an absolutely perfectly tweaked spring animation for your app using nothing but default UIView.animate parameters. Let’s say, hypothetically, that the code is something like this:</p><pre>UIView.animate(<br>    withDuration: 0.4,<br>    delay: 0,<br>    usingSpringWithDamping: 0.8,<br>    initialSpringVelocity: 1.75,<br>    options: [.curveEaseInOut, .beginFromCurrentState, .allowUserInteraction]<br>) {<br>    self.view.center.y += 50<br>} completion: { (isCompleted) in<br>    // completion code<br>}</pre><p>Well, if you absolutely nail it and you want to start using this animation everywhere in your app, you suddenly start seeing yourself copying and pasting this snippet everywhere. And we both know that copy-pasted code is no good.</p><p>Many people try to solve this by creating a centralized “constants” type, where one can store their perfectly-tuned parameters and quickly grab them when needed. Something like this:</p><pre>UIView.animate(<br>    withDuration: AnimationDefaults.defaultDuration,<br>    delay: 0,<br>    usingSpringWithDamping: AnimationDefaults.defaultSpringDamping,<br>    initialSpringVelocity: AnimationDefaults.defaultSpringVelocity,<br>    options: AnimationDefaults.defaultOptions<br>) {<br>    self.view.center.y += 50<br>} completion: { (isCompleted) in<br>    // completion code<br>}</pre><p>But while this is better than copy-pasted code, it’s still <em>somewhat</em> the same. Cause if one day you’ll want to migrate your animations to UIViewPropertyAnimator, well… you’ll have yourself a problem.</p><h4>The Solution: “Animator” struct</h4><p>Instead, here’s what we can come up with. First, let’s create a new type called Animator:</p><pre>struct Animator {<br>    typealias Animations = () -&gt; ()<br>    typealias Completion = (Bool) -&gt; ()<br>    <br>    let perform: (@escaping Animations, Completion?) -&gt; ()<br>}</pre><p>And with that, a simple extension on a UIView to mirror the usual UIView.animate as closely as possible:</p><pre>extension UIView {<br>    static func animate(with animator: Animator, animations: @escaping () -&gt; (), completion: ((Bool) -&gt; ())? = nil) {<br>        animator.perform(animations, completion)<br>    }<br>}</pre><p>I hope by now you understand where we’re going with this. Now adding a reusable, perfectly tuned spring animation is simple:</p><pre>extension Animator {<br>    static let defaultSpring = Animator { (animations, completion) in<br>        UIView.animate(<br>            withDuration: 0.4,<br>            delay: 0,<br>            usingSpringWithDamping: 0.8,<br>            initialSpringVelocity: 1.75,<br>            options: [.curveEaseInOut, .beginFromCurrentState, .allowUserInteraction],<br>            animations: animations,<br>            completion: completion<br>        )<br>    }<br>}</pre><p>So as you see, we’re simply wrapping our spring animation in a reusable block of code. The usage?</p><pre>UIView.animate(with: .defaultSpring) {<br>    self.view.center.y += 50<br>} completion: { (isCompleted) in<br>    // completion code<br>}</pre><p>Now it’s not just reusable, but also incredibly clean and easy to read.</p><p>With this technique, you can create as many reusable animations as you want.</p><h4>One More Thing</h4><p>Often, you want your methods that perform animations to have an animated: Bool parameter. Well, you probably remember how much of a pain that is with custom animations, right? Fret no more, here’s a neat little trick using the Animator:</p><pre>extension Animator {<br>    static let noAnimation = Animator { (animations, completion) in<br>        animations()<br>        completion?(true)<br>    }<br>}</pre><p>And the usage:</p><pre>func moveView(animated: Bool) {<br>    UIView.animate(with: animated ? .defaultSpring : .noAnimation) {<br>        self.view.center.y += 50<br>    } completion: { (isCompleted) in<br>        // completion code<br>    }<br>}</pre><p>Hopefully, you find this as cool as I do.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2d10713ca3a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The ultimate solution to the “how on Earth do I host and show this Privacy Policy” problem]]></title>
            <link>https://olegdreyman.medium.com/the-ultimate-solution-to-the-how-on-earth-do-i-host-and-show-this-privacy-policy-problem-cd185ddab6dd?source=rss-7872af0bbed1------2</link>
            <guid isPermaLink="false">https://medium.com/p/cd185ddab6dd</guid>
            <category><![CDATA[web]]></category>
            <category><![CDATA[libraries]]></category>
            <category><![CDATA[swift-programming]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[uikit]]></category>
            <dc:creator><![CDATA[Oleg Dreyman]]></dc:creator>
            <pubDate>Fri, 19 Feb 2021 10:01:00 GMT</pubDate>
            <atom:updated>2022-07-19T22:41:09.908Z</atom:updated>
            <content:encoded><![CDATA[<h4>Introducing TelegraphKit</h4><figure><img alt="Screenshot of the web article displayed in the iOS App via TelegraphKit" src="https://cdn-images-1.medium.com/max/1024/1*bzKa0FDjndRvsxAIsmioOw.png" /></figure><p>It’s a well-known fact that app development (like any job) has its fair share of mundane tasks. Tasks like creating Legal pages, support screens, and settings screens, to name a few.</p><p>Let’s talk about adding a Privacy Policy. <em>Maybe</em> you already have written it (great work!), now how are you going to display it?</p><p>Should you just use a giant UITextView? What if the privacy policy changes? You always need to have your Privacy Policy up to date. It needs to be backend-editable.</p><p>Oh no, now what: buying a domain? Hosting a website? Choosing a CMS? For many apps, this is overkill.</p><p>I usually try to make these processes as simple and as streamlined as possible. So we usually host our Privacy Policies on <a href="https://telegra.ph"><strong>Telegra.ph</strong></a>.</p><p><strong>Telegraph</strong> is an anonymous publishing tool that lets you create richly formatted posts with photos and all sorts of embedded stuff. To create something on <strong>Telegraph</strong>, it literally takes ten seconds:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WbvIsy1tDYjnCj5rCbFEJg.gif" /></figure><p>No account required. Zero configuration. Open the page, type, press Publish and copy the link. That’s it. (<em>You can edit these pages in the same browser you’ve created them. To make it more robust, you can also </em><a href="https://telegram.org/blog/telegraph"><em>link them to your Telegram account</em></a>).</p><h4>Introducing TelegraphKit</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JLDCc-yi1m2lVnQnsDtTAA.png" /></figure><p>You could put this in a WKWebView as-is and call it a day, and it would still be a good experience (Telegra.ph pages load <em>incredibly</em> fast). But I went a little bit further.</p><p>The goal was for the end result to be simple and convenient for the developers, and delightful for the users. It felt important to present these pages in a way that looked and felt native.</p><p>Here’s what I was able to achieve (default Telegra.ph appearance vs. <strong>TelegraphKit</strong>):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kAuJwiZmTruaBOmmKBWmlA.jpeg" /><figcaption>Default Telegra.ph look (left) vs TelegraphKit (right)</figcaption></figure><p>Before I go into any details, let me first note that I’ve published this code as a Swift package, and it’s available on GitHub now:</p><p><a href="https://github.com/dreymonde/TelegraphKit">GitHub - dreymonde/TelegraphKit: The ultimate solution for showing ad hoc, server-editable web content (FAQs, Tutorials, Privacy Policy, etc.) in your iOS apps</a></p><p>Now, what exactly is it?</p><h4>Some dark WKWebView magic</h4><p>Quite a few improvements were made, but these two are the most obvious:</p><ul><li>Dark mode support</li><li>System fonts and styles</li></ul><p>It really has a native look &amp; feel, which is lovely. And what’s interesting is that even though it looks native, it’s still just WKWebView under the hood! Just modified extensively to make it look just right.</p><p>To achieve that, I used a function called evaluateJavaScript to extend and modify the page’s look using <a href="https://www.w3schools.com/html/html_styles.asp">HTML Styles</a>. To support dark mode, the code simply reacts to the appearance change by turning on and off the corresponding “dark appearance” style. I’ve discovered this technique in this <a href="https://indiestack.com/2018/10/supporting-dark-mode-in-app-web-content/">fantastic article by Daniel Jalkut</a>, and it lies in the core of <strong>TelegraphKit</strong>.</p><p>To be fully satisfied with the look, hours were spent digging through Telegraph’s internal HTML structure with Chrome’s debugger. There was a lot of trial &amp; error in the experiments. In the end, the result was well worth it.</p><p>You can check out the usage docs on the <a href="https://github.com/nicephoton/TelegraphKit">GitHub page</a>, but it essentially boils down to this:</p><pre>import TelegraphKit<br><br>let url = URL(string: &quot;&lt;your-telegraph-url&gt;&quot;)!<br>let telegraphVC = <strong>TelegraphViewController(url: url)</strong><br>let nvc = UINavigationController(rootViewController: telegraphVC)<br>self.present(nvc, animated: true)</pre><p>TelegraphKit is now my go-to solution to show any informational content I need to create quickly and edit remotely. Beyond using it to display a Privacy Policy, I also use it to display Support pages, Guides, “What’s New” notes, and much more. It’s a highly valuable tool in my toolset that saves me tons of time. Hopefully it will be helpful for you as well!</p><p><a href="https://github.com/dreymonde/TelegraphKit">GitHub - dreymonde/TelegraphKit: The ultimate solution for showing ad hoc, server-editable web content (FAQs, Tutorials, Privacy Policy, etc.) in your iOS apps</a></p><p>I’m very excited about launching <strong>TelegraphKit</strong>, and excited to hear your thoughts and opinions on it!</p><p>Please try it out and let me know how it goes! Do you find it useful? Is there anything that you’re missing? Do you want it to have any other features, or to have even more flexibility and customization? Use the comments section below, open a GitHub issue or drop me a line at <a href="mailto:oleg@dreyman.dev">oleg@dreyman.dev</a>, I’ll be happy to help!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cd185ddab6dd" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[An easier way to create dates in Swift]]></title>
            <link>https://olegdreyman.medium.com/an-easier-way-to-create-dates-in-swift-c2b2aaf6167?source=rss-7872af0bbed1------2</link>
            <guid isPermaLink="false">https://medium.com/p/c2b2aaf6167</guid>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[calendar]]></category>
            <category><![CDATA[uikit]]></category>
            <category><![CDATA[libraries]]></category>
            <dc:creator><![CDATA[Oleg Dreyman]]></dc:creator>
            <pubDate>Thu, 04 Feb 2021 14:16:49 GMT</pubDate>
            <atom:updated>2022-07-19T22:34:27.343Z</atom:updated>
            <content:encoded><![CDATA[<h4>Introducing DateBuilder</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i3JtZkLtudSaxcFICn_prw.png" /></figure><p><strong>TL;DR? I’ve open sourced </strong><a href="https://github.com/nicephoton/DateBuilder"><strong>DateBuilder</strong></a><strong>, a powerful date calculation library. Check out the README to see all the incredible stuff it can do.</strong></p><p>Apple’s Calendar API is amazing, it is. It’s quite obscure in some parts, true, and it requires quite a little bit of learning and adjusting, but it’s very effective, very expansive, and, most of all, very <em>correct</em>. If you do everything right, it works great and it handles all the weird edge cases for you.</p><p>But still, when you need to create very specific dates it can get a little tricky. Expressing even something as simple as “tomorrow at 8pm” requires you to know <em>exactly</em> what you’re doing, and calculating something like “first day of next week” will send you down a 30-minute Google road exploring billions of different solutions, most of which are wrong (because yes, believe it or not, there are places in the world where the week <em>doesn’t</em> start on Sunday).</p><h4>Introducing DateBuilder</h4><p>To make the power of Calendar and date calculations available to everyone, I’ve now open sourced <a href="https://github.com/nicephoton/DateBuilder"><strong>DateBuilder</strong></a>. It’s a powerful date creation engine that lets you create Date and DateComponents objects with incredible ease using a very visual, declarative syntax.</p><p><a href="https://github.com/nicephoton/DateBuilder">nicephoton/DateBuilder</a></p><p>I built this while working on my upcoming local notification library. For the scheduling component I wanted something very expressive, very concise and easy-to-understand.</p><p>For example, at one point I needed to schedule a notification for every first day of the week, with a sort of “weekly review”. With <strong>DateBuilder</strong>, it’s incredibly easy:</p><pre>EveryWeek(forWeeks: 50, starting: .thisWeek)<br>    .firstDay<br>    .at(hour: 10, minute: 00)<br>    .dateComponents() // [DateComponents]</pre><p>One can then use this array of DateComponents to schedule a bunch of notifications, easy as that.</p><p>I won’t go into much detail about the API in this post, because there’s a whole <a href="https://github.com/nicephoton/DateBuilder">README on GitHub</a>! Just take a brief look to see all the stuff <strong>DateBuilder</strong> can do.</p><p>I’m very excited about launching this new open source package, and excited to hear your thoughts and opinions on it! I’m sure it’ll be quite useful for many many apps out there.</p><p>Please try it out and let me know how it goes! Do you find it useful? Is there anything that you’re missing? Do you want it to have any other features, or to have even more flexibility? Use the comments section below, open a GitHub issue or drop me a line at <a href="mailto:oleg@dreyman.dev">oleg@dreyman.dev</a>, I’ll be happy to help!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c2b2aaf6167" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing ScheduledNotificationsViewController]]></title>
            <link>https://olegdreyman.medium.com/introducing-schedulednotificationsviewcontroller-67c8b73813e3?source=rss-7872af0bbed1------2</link>
            <guid isPermaLink="false">https://medium.com/p/67c8b73813e3</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[articles]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[notifications]]></category>
            <dc:creator><![CDATA[Oleg Dreyman]]></dc:creator>
            <pubDate>Wed, 27 Jan 2021 19:36:17 GMT</pubDate>
            <atom:updated>2022-06-14T07:57:06.716Z</atom:updated>
            <content:encoded><![CDATA[<h4>The best way to debug your local notifications</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TElzg9rhoauDaQ_ZlgT0PA.png" /></figure><p><strong>TL;DR? Check out our </strong><a href="https://github.com/nicephoton/ScheduledNotificationsViewController"><strong>new Swift package</strong></a><strong>, it helps you debug local notifications with ease and elegance.</strong></p><p>At my company we’ve had a few projects recently where we had to implement a sophisticated and sometimes quite complex local notifications system. It’s always tricky to get right, because there are always tons and tons of moving parts. It’s very nuanced, as one would expect.</p><p>Making sure that your code works as expected is not always easy. Say you’ve scheduled notifications for the next 40 days — now what? Should you wait 40 days to make sure that they get delivered as intended? I didn&#39;t think so.</p><p>That’s why recently I’ve created a simple debug screen that lets you see <em>all</em> of the pending local notifications in one place. All the data you could possibly need is there: next trigger date, identifiers, categories, and so on.</p><p>It proved extremely useful to be able to check if your notifications are scheduled properly right on your device or the simulator, without having to juggle breakpoints or searching the logs. It definitely helped to discover at least a few bugs that otherwise would’ve been extremely easy to miss.</p><p>The coolest feature I’ve added was <em>simulating the notification delivery</em>. So when we tap on any scheduled notification, its exact copy will be delivered <em>immediately</em>! It feels like magic:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RpCXRLWD0DpzWhn9VMHhJA.gif" /></figure><p>This helps to see exactly how our notification will look once it’s delivered. And it doesn’t “replace” the scheduled notification too — the “real” one will still be delivered as planned!</p><h4>This thing was so cool, that I ultimately decided to open source it</h4><p>I quickly realized just how useful this screen was, and how many hours of development and debugging time it saved me. It is fairly self-contained and app-agnostic, so I knew I had to open source it and share it with all of you! I’m absolutely sure you’ll love this.</p><p>So, <strong>ScheduledNotificationsViewController</strong> is now available as a SwiftPM package on GitHub! Get it here:</p><p><a href="https://github.com/dreymonde/ScheduledNotificationsViewController">GitHub - dreymonde/ScheduledNotificationsViewController: See all your scheduled local notifications in one place</a></p><p>It has no dependencies and a minimal API. It’s as “plug and play” as it gets. No configuration, no setup: just create a ScheduledNotificationsViewController() instance and you’re good to go.</p><p>Please try it out and let me know how it goes! Do you find it useful? Is there anything that you’re missing? Do you want it to have any other features, or to show more data? Use the comments section below, open a GitHub issue or drop me a line at <a href="mailto:oleg@dreyman.dev">oleg@dreyman.dev</a>, I’ll be happy to help!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=67c8b73813e3" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>