Notes on PlantUML

PlantUML

Installation

  1. You’ll want Homebrew
  2. brew install swiftplantuml
  3. brew install plantuml

Warning!!!

By default PlantUML uses a remote servce to generate images of .puml files. You can read more at plantuml.com/faq

Links to png or svg generated by PlantUML Server are valid forever (that is as long as the server is up). However, we do not store any diagrams on our servers. This may sound contradictory. It is not: the whole diagram is compressed into the URL itself. When the server receives the URL, it decompresses the URL to retrieve the diagram text and generates the image. There is no need to store anything. Even if the server is down, you can retrieve the diagram using the flag -decodeurl with the command line. Furthermore, the diagram data is stored in PNG metadata, so you can fetch it even from a downloaded image.


Make a Class Diagram

While this seems reasonable I want to generate the images locally. There makes for a two-step process:

  1. Generate .puml
  2. Generate .png

1. Generate .puml

swiftplantuml --output consoleOnly MyClass.swift > my-class.puml

2. Generate .puml

plantuml will crop the image if the size limit it set higher than needed.

plantuml -DPLANTUML_LIMIT_SIZE=14000 my-class.puml -png


References

PlantUML Homgepage

Excellent Blog Post

SwiftPlantUML


Getting Started

plantuml -help

SpacePod 18 Decoding & Formatting Dates

SpacePod 18 Decoding & Formatting Dates

Goals

  1. Decode date as a Date?
  2. Format date nicely like this March 26, 2007
  3. Sort and Query by date
  4. Eventually cache data and images in CoreData

## Resources

YouTube GitHub

Steps

  1. Date Formatters
  2. Date Decoding Strategy

DateFormatter+Extensions.swift

extension DateFormatter {

    /// Formats date as "yyyy-MM-dd" (e.g. 2021-12-18)
    static let yyyyMMdd: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        formatter.calendar = Calendar(identifier: .iso8601)
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        return formatter
    }()

    /// Formats date as "Month Day, Year" (e.g. December 18, 2021)
    static let longDate: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .long
        formatter.timeStyle = .none
        formatter.calendar = Calendar(identifier: .iso8601)
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        return formatter
    }()
}

DateDecodingStrategy+Extensions

// add DateDecodingStrategy+ Extensions.swift
extension JSONDecoder.DateDecodingStrategy {
    /// Decodes Date in as yyyy-MM-dd (e.g. 2021-12-18)
    static var yyyyMMdd: JSONDecoder.DateDecodingStrategy {
        return JSONDecoder.DateDecodingStrategy.formatted(DateFormatter.yyyyMMdd)
    }
}

Data+Extensions

var toPod: Pod? {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        decoder.dateDecodingStrategy = .yyyyMMdd
        return try? decoder.decode(Pod?.self, from: self)
    }

    var toPods: [Pod]? {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        decoder.dateDecodingStrategy = .yyyyMMdd
        return try? decoder.decode([Pod]?.self, from: self)
    }

Pod.swift

Change the date property from type String to Date?

let date: Date?

and then modify default

date: DateFormatter.yyyyMMdd.date(from: "yyyy-MM-dd"),

PodDetailView.swift

Label(pod.date?.description ?? "", systemImage: "calendar")

Data+Extensions

var toPods: [Pod]? {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        decoder.dateDecodingStrategy = .yyyyMMdd
        return try? decoder.decode([Pod]?.self, from: self)
    }

Date+Extensions.swift

extension Date {
    /// Returns a date string as "Month Day, Year" (e.g. December 18, 2021)
    var long: String {
        return DateFormatter.longDate.string(from: self)
    }
}

PodDetailView.swift

Label(pod.date?.long ?? "", systemImage: "calendar")

macOS Commands - Homebrew

Homebrew

“The Missing Package Manager for macOS.”

While Homebrew isn’t strictly necessary, it does make installing and maintaining command line apps a breeze.

Home Page Github

Installation

It’s always a good idea to read an install script before installing.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Basics

brew update to update Homebrew. brew upgrade to upgrade packages and formulas.

Documentation

manpage

SpacePod 17: Adding and Ignoring App Secrets

SpacePod 17 - App Secrets

Thoughts

Lot’s of ways to do this, and all of them have drawbacks. We are doing it this way to keep things simple. Warning, this approach is simple, but the api key will in plaintext in the app bundle!!!

Other Methods

  1. Environment setting
  2. CI/CD
  3. Shared iCloud database
  4. Bypass the API altogether

Objectives

  1. Use your api key instead of the demo key
  2. Make sure we don’t check our api key into the repo

Steps

  1. Update Network.swift
  2. Create .gitignore
  3. Copy & paste from github
  4. Add Secrets.json to .gitignore?
  5. Add Secrets.json to project and target
  6. Create Secrets.swift model
  7. Load apiKey from Secrets.txt ?? DEMO_KEY

Secret.swift

  1. New Group “Models”
  2. New Type Secret
import Foundation

struct Secret: Codable {
    let apiKey: String
}

Decoding

var toSecret: Secret? {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    return try? decoder.decode(Secret.self, from: self)
}

Update Network

Delete getString() & getPod()

import Foundation

class Network {

    let baseUrl = "https://api.nasa.gov/planetary/apod"
    let apiKey = "?api_key=" + (File.data(from: "Secrets", withExtension: .json)?.toSecret?.apiKey.description ?? "DEMO_KEY")

    func getPods() async -> [Pod]? {
        let url = URL(string: "\(baseUrl)\(apiKey)&count=20")!
        do {
            let request = URLRequest(url: url)
#if DEBUG
            print("🌎 request: " + request.debugDescription)
#endif
            let (data, response) = try await URLSession.shared.data(for: request)
#if DEBUG
            print("🌎 response: " + response.debugDescription)
#endif
            return data.toPods
        }
        catch {
#if DEBUG
            print("🌎 error: " + error.localizedDescription)
#endif
            return nil
        }
    }
}

Create .gitignore

  1. Add a new empty file at the root of the project and name it “.gitignore”. Be sure to notinclude it in our target.
  2. Copy and paste the contents of Swift.gitignore into .gitignore.
  3. Add Secrets.json to .gitignore
# Project
Secrets.json

Add Secrets.json

  1. Select the project
  2. Right click to create a new file named Secrets.json
  3. Be sure to include it with SpacePod target
  4. Add the following json
  5. Be sure to change apiKey to your api key from https://api.nasa.gov and save
{
    "api_key" : "PASTE_YOUR_API_KEY_HERE"
}

SpacePod 16b - Bug Fixes

SpacePod 16b - Bug Fixes

Last time we accidently introduced a bug where the selected pod details can be overwritten by our default pod.

Objectives

  1. Fix Pod.default bug
  2. Fix Preview
  3. Fix Task reloading bug

Steps

Replace default

In PodDetailView.swift replace = Pod.default with : Pod.

Fix Preview

Load and decode get-pod.json, and pass it to our view.

struct PodDetailView_Previews: PreviewProvider {
    static var pod = File.data(from: "get-pod", withExtension: .json)?.toPod
    static var previews: some View {
        PodDetailView(pod: pod!)
    }
}

Delete Task

Delete task as we are passing the data from PodListView.swift.

	.task {
            if let response = await Network().getPod() {
                pod = response
            }
        }

PodDetailView.swift

import SwiftUI

struct PodListView: View {
    @State var pods: [Pod] = []

    var body: some View {
        NavigationView {
            List {
                ForEach(pods, id: \.self) { pod in
                    NavigationLink(destination: PodDetailView(pod: pod)) {
                        Text(pod.title)
                    }
                }
            }
            .navigationTitle(pods.isEmpty ? "Fetching Pods..." : "SpacePod")
            .task {
                if pods.isEmpty { await getPods() }
            }
            .refreshable {
                await getPods()
            }
        }
    }

    private func getPods() async {
        if let response = await Network().getPods() {
            pods = response
        }
    }
}

struct PodListView_Previews: PreviewProvider {
    static var pods = File.data(from: "get-pods", withExtension: .json)?.toPods
    static var previews: some View {
        PodListView(pods: pods!)
    }
}

Recently picked up: Courage Is Calling: Fortune Favors the Brave on a whim at Commonplace Books, and so far so good. 📚

SpacePod 01: Project Overview

Let’s build a SwiftUI app that displays the NASA photo of the day. Updates will be tiny and on YouTube and GitHub

import SwiftUI

struct ContentView: View {
    var body: some View {
        List {
            MyButton(title: "Button 1")
            MyButton(title: "Button 2")
        }
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
struct MyButton: View {
    
    var title: String
    
    @State private var showSheet = false
    
    var body: some View {
        Button(action: {
            showSheet.toggle()
        }, label: {
            Text(title)
        }).sheet(isPresented: $showSheet, content: {
            Text("Sheet for \(title)")
        })
    }
}

Privacy Policy

Philosophy

  1. You have a fundamental right to privacy.
  2. Your data is yours (not ours).

Policies

  1. We will only collect data with your consent.
  2. We will retain information only for as long as needed.
  3. We will not share information with 3rd parties unless required by law.
  4. We do not knowingly collect data about children.
  5. This site will be updated as our policies evolve.

Collected Data

  1. Purchases a. Used for Analytics, and App Functionality b. Linked to the user’s identity