Post

MK Downloader progress log - Part 1

This is the first in a series of planned logs of my current project MK Downloader, that I’ve begun this week. I’ve spent roughly 2 hours a day writing code for this project (and almost an equal amount of time thinking of various problems I encountered and looking for solutions) since June 26, 2022, and I’m mentioning some outputs achieved and some lessons learnt so far.

Intro

I chose this project because I prefer an iPad to a laptop whenever possible. Its got decent apps, is good for reading and watching movies and has an excellent battery life. Most of all, I like that its an always-on device. But it also has Safari, and all its limitations - a prominent one of which is unreliable downloading, which this project aims to address. For more details, please refer to below links:

Outputs

  • Basic app is now ready and works flawlessly on both iPad (tested on simulator and real iPad) and iPhone (tested on simulator only). Downloads work and show up in the MK Downloader folder in the Files app.

Lessons learnt

  • SwiftUI issues: I chose SwiftUI for UI development, because its cross platform and my app is not too complicated, so it fits easily. But there are some things to lookout for. I tried to create a List item with some buttons like pause/resume and delete. I also wanted that clicking anywhere else on the list item should produce a sheet with advanced download options. Here’s a sample code:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
    /// The code below is a weird hack to get List item and buttons working properly.
    /// Clicking anywhere in list item except label triggers the function of `Advanced` button
    /// Also, notice that I use Label for `Pause/Rename`, instead of `Button`, which is a strange necessity.
      
    struct DownloadItem: View {
        /// ... lot of state variables and bindings
          
        var body: some View {
            HStack {
                ZStack {
                    VStack(alignment: .leading) {
                        Text("\(item.filename ?? "<Unknown>")").bold()
                    }
                    Button {
                        isSheetPresented = true
                    } label: {
                        Label("Advanced", systemImage: "gearshape.2").labelStyle(IconOnlyLabelStyle())
                    }.opacity(0)
                }
                Label("Pause/Resume", systemImage: currentDownloadStatus == .paused ? "play.fill" : "pause.fill").labelStyle(IconOnlyLabelStyle())
                    .onTapGesture {
                        print("On tap")
                        if currentDownloadStatus == .paused {
                            currentDownloadStatus = .running
                        } else if currentDownloadStatus == .running {
                            currentDownloadStatus = .paused
                        }
                    }
            }
        }
    }
    
  • Tried and tested ways are generally better: I initially went with async/await URLSession methods for downloading files, since that’s what I had originally learnt and because its less code and obvious to follow. But these methods are not as feature-rich as URLSessionDelegate. For e.g. no background downloading support in async/await approach (e.g. when app is minimized) and better events and error notifications through delegates. When Apple has a dedicated documentation page on Downloading Files from Websites, you are better off following it.
  • Xcode issues: While working on the project, I had to add entitlements for two things: UIFileSharingEnabled for showing the MK Downloader folder in Files app and NSAppTransportSecurity with NSAllowsArbitraryLoads for supporting HTTP-only downloads. Both times, updating the target project configuration in Info tab wasn’t enough. I tried cleaning the build folder and even restarting Xcode to no avail. Weirdly, upon repeated tries, the error just went away. Xcode definitely one-upped Albert Einstein here.
  • Code separation should be planned early on: While I’ve made a habit of writing separate views per file, I often end up mixing UI and backend logic in the interest of being pragmatic and shipping early, especially with SwiftUI. While working, I ended up writing lots of networking code and download management code in a single file. Once I began refactoring the code, I ended up inadvertantly introducing an issue that broke UI updates. Took me an hour and a pizza to finally figure things out.

Planned for next time

  • Background downloading support is just not there yet. Though I hope to get it done by next progress log.
  • Error handling in UI. The goal is to have have error handling and presentation in such a way that errors should become apparent to user (only me for now), and should require almost no looking at logs on part of the developer (definitely me).

That’s it. Happy weekend. :smile:

This post is licensed under CC BY 4.0 by the author.