ios, web, thoughts & notes (rss)

dreamscape by isak solheim

  • 15 nov 2023 ~

    Analyzing analytics

    156 views, that’s how many page views this website racked up last week! Most people drop in via direct link, then we have Medium at nr. 2 for referring people to my website. Some people also came from theforest.link which I had totally forgotten my website was added to :P Some people were referred through the websites of my friends (see left sidebar) which is really fun to see!

    Most of the traffic came from Norway, which is expected since most visitors likely know about this website through word of mouth. Then Thailand came in clutch for the second place, beating Switzerland which was close by. Hello people from Thailand and Switzerland :D

    On another note

    I’ve set up a notes page, check it out! And a lot of the books from my 2022 bookshelf have fallen out and disappeared 🤔

  • 9 nov 2023 ~

    Is anybody out there?

    People! I’m very curious, who is reading this? If you happen to be doing so, I have a question for you. What is your favorite way of interacting with blogs? I personally like email, and I’d be very happy if you would say hello! And also, it’s time for an experiment:

    Yesterday I added Tinylytics to this website, and I’ll now place a guess on how many people stop by here during the next 7 days. I know that this website has bad SEO (page 2 google bad), but I do link to here from multiple places (GitHub, LinkedIn, Medium). I have two posts on Medium which both gets read a lot, so I would expect most of my traffic coming from there. I’m guessing somewhere around 15 unique visitors and 40 or so page hits every week. And I’m most excited to see if I get any email.

    Until next week!

  • 22 sept 2023 ~

    isak dot me slash reading

    I finally got around to creating a /reading page, go check it out! The page is automatically built with the data from my goodreads account, more on how I did this later..

  • 4 sept 2023 ~
  • 10 aug 2023 ~

    Et bra algdat tips

    Faget Algoritmer og Datastrukturer er gammelt, alle har hatt det (tror jeg). Faget blir per dags dato undervist av Magnus Lie Hetland. Jeg syntes han er flink og anbefaler alle til å møte opp i forelesningene. I tillegg har han en kul nettside. Forrige semester var forelesningene på onsdager, så man kunne kjøpe kanelboller i kantina for 20kr i pausen. Som prepp til eksamen kan jeg anbefale å se youtubevideoer av sorteringsalgoritmer visualisert som ungarsk folkedans (lenke).

  • 4 aug 2023 ~
  • 3 aug 2023 ~

    July 2023

    Bla bla bla, July was great!

    I’ve decided to stop writing montly recaps. Here is a picture of Amund as seen through a beer in Helsinki:

    amund as seen through a beer
  • 27 jul 2023 ~
  • 3 jul 2023 ~

    June 2023

    I finished my final exams, packed up all my stuff and headed off on a train to Oslo for the summer. It felt great 🙌 In Oslo I’ve been back at the office working on Oslonøkkelen. I’ve also been outside skateboarding a bunch. Here are some pictures from this month:

    climbing a ladder from kayak
    lademoen kirke in the snow nuutti and amund look at lademoen kirke

    I finished The Two Towers and I’m close to finishing Return of the King. I’m getting exited to start reading something else now :P

  • 1 jul 2023 ~
  • 28 jun 2023 ~

    May 2023

    riding my bike

    Suuuper busy, but great month! Trondheim has had back-to-back nice weather for two months now, and I spent a lot of time outdoors 🙌

    But being May, I had to study for my final exams, which meant I had little to spend programming. It often feels like studying computer science is the biggest obstacle stopping me from writing code!

    I read a little less this month, finishing The Fellowship of the Ring and making some progress reading The Two Towers. I also saw Squarepusher playing at Jazzfest. Here is a picture of my roommate Nuutti delivering his bachelors:

    nuutti delivering his bachelors
  • 19 jun 2023 ~

    uistackview extensions

    import UIKit
    
    extension UIStackView {
        func addArrangedSubviews(_ views: [UIView]) {
            views.forEach { addSubview($0) }
        }
    }
    stack.addArrangedSubviews([headerLabel, textLabel, errorLabel, buttonStack])
  • 13 jun 2023 ~

    ios presentation

    controller.dismiss(animated: true)
    self.coordinator.perform(action: .login, in: self)
    controller.dismiss(animated: true) {
      self.coordinator.perform(action: .login, in: self)
    }
  • 12 jun 2023 ~

    NSKeyedUnarchiver.unarchivedObject()

    ‘unarchiveTopLevelObjectWithData’ was deprecated in iOS 12.0: Use unarchivedObject(ofClass:from:) instead

    [general] *** -[NSKeyedUnarchiver validateAllowedClass:forKey:] allowed unarchiving safe plist type ''NSNumber' (0x1b9e88aa0)
      [/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework]'
      for key 'root', even though it was not explicitly included in the client allowed classes set:
      '{(
        "'NSData' (0x1b9e74490) [/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework]"
      )}'
      This will be disallowed in the future.
    do {
      guard let decodedData = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSData.self, NSNumber.self], from: data) as? T else {
        return .failure(.decode)
      }
      return .success(decodedData)
    } catch {
      return .failure(.decode)
    }
  • 27 may 2023 ~

    Roll-001

    Two days ago I developed my first roll of film! My friend Martin taught me how to load the film reel, and after some practice with the lights turned on, we entered the darkroom. Three attempts later, the film was loaded into the developing tank. Here are some of the photos, all taken on my Ricoh R10 with a roll of HP5 Plus:

    lademoen kirke
    lademoen kirke from the front
    lademoen kirke in the snow nuutti and amund look at lademoen kirke
    people outside samfundet
    portrait of isak portrait of martti
    my bike nuutti on a skateboard
  • 1 may 2023 ~

    April 2023

    isak and solveig

    April came with some great weather, so I spent the first week skating outside in the sun with Nuutti, Solveig, Håvard and Sveinung. It was great!

    Reading

    I read two books this month, Bicycle Diaries by David Byrne and The Hobbit by Tolkien. I’m also close to finishing The Fellowship of the Ring which I’ve been really enjoying, and I’m excited to read the rest of The Lord of the Rings series in May!

    Programming

    I’ve had a lot of fun creating a prototype for a game using Godot 4. The backlog of stuff I want to complete is growing as usual :P

  • 6 apr 2023 ~

    Migrating from ZSH to Fish

    Fish greeting

    I’ve always been using ZSH + Oh My Zsh for my shell configuration, which I have been happy with. But I recently got the urge to try out something new, and I decided to try out fish as it includes many useful features without needing extra configuration. It also has a sweet name.

    Fish

    Fish is not POSIX compliant, meaning you cannot use it to run your regular bash scripts. Fish-programs has to be written using the fish syntax, which seems easy enough to learn. I do little bash scripting for my regular workflow, and I plan on using fish solely as an interactive shell, so I do not think I will notice this very much

    Installation

    I used homebrew for a quick and easy installation:

    brew install fish

    I then added fish to my list of known shells, and used chsh to set fish as default. After doing these steps, I had to sign out and back in again.

    sudo bash -c 'echo $(which fish) >> /etc/shells'
    chsh -s $(which fish)

    I also had to use the fish_add_path command for fish to recognize programs installed by homebrew:

    fish_add_path /opt/homebrew/bin

    Configuring fish

    As I mentioned earlier, fish is very useable without any configuration, which I like. I, therefor, have decided to keep my config as clean as possible, without relying on any plugins.

    The first thing I did was to set the fuck alias, and disable the autosuggestion by default, as I found it distracting. I did this in .config/fish/config.fish.

    # ~/.config/fish/config.fish
    
    thefuck --alias | source
    set -g fish_autosuggestion_enabled 0

    I then found a pretty greeting-function and modified it a bit to only print the greeting for large windows that open in ~, as I do not want it cluttering the screen while I’m working on something.

    # ~/.config/fish/functions/fish_greeting.fish
    
    function fish_greeting
      if test $LINES -gt 25; and [ (pwd) = $HOME ]
        echo '                 '(set_color F00)'___
      ___======____='(set_color FF7F00)'-'(set_color FF0)'-'(set_color FF7F00)'-='(set_color F00)')
    /T            \_'(set_color FF0)'--='(set_color FF7F00)'=='(set_color F00)')    '(set_color red)(whoami)'@'(hostname)'
    [ \ '(set_color FF7F00)'('(set_color FF0)'0'(set_color FF7F00)')   '(set_color F00)'\~    \_'(set_color FF0)'-='(set_color FF7F00)'='(set_color F00)')'(set_color yellow)'    Uptime: '(set_color white)(uptime | sed 's/.*up \([^,]*\), .*/\1/')(set_color red)'
     \      / )J'(set_color FF7F00)'~~    \\'(set_color FF0)'-='(set_color F00)')    IP Address: '(set_color white)(ipconfig getifaddr en0)(set_color red)'
      \\\\___/  )JJ'(set_color FF7F00)'~'(set_color FF0)'~~   '(set_color F00)'\)     '(set_color yellow)'Version: '(set_color white)(echo $FISH_VERSION)(set_color red)'
       \_____/JJJ'(set_color FF7F00)'~~'(set_color FF0)'~~    '(set_color F00)'\\
       '(set_color FF7F00)'/ '(set_color FF0)'\  '(set_color FF0)', \\'(set_color F00)'J'(set_color FF7F00)'~~~'(set_color FF0)'~~     '(set_color FF7F00)'\\
      (-'(set_color FF0)'\)'(set_color F00)'\='(set_color FF7F00)'|'(set_color FF0)'\\\\\\'(set_color FF7F00)'~~'(set_color FF0)'~~       '(set_color FF7F00)'L_'(set_color FF0)'_
      '(set_color FF7F00)'('(set_color F00)'\\'(set_color FF7F00)'\\)  ('(set_color FF0)'\\'(set_color FF7F00)'\\\)'(set_color F00)'_           '(set_color FF0)'\=='(set_color FF7F00)'__
       '(set_color F00)'\V    '(set_color FF7F00)'\\\\'(set_color F00)'\) =='(set_color FF7F00)'=_____   '(set_color FF0)'\\\\\\\\'(set_color FF7F00)'\\\\
              '(set_color F00)'\V)     \_) '(set_color FF7F00)'\\\\'(set_color FF0)'\\\\JJ\\'(set_color FF7F00)'J\)
                          '(set_color F00)'/'(set_color FF7F00)'J'(set_color FF0)'\\'(set_color FF7F00)'J'(set_color F00)'T\\'(set_color FF7F00)'JJJ'(set_color F00)'J)
                          (J'(set_color FF7F00)'JJ'(set_color F00)'| \UUU)
                           (UU)'(set_color normal)
      end
    end

    I also customized the default prompt. I took inspiration from the prompt I had been using from Oh My Zsh (avit), and created a similar looking one for fish:

    # ~/.config/fish/functions/fish_prompt.fish
    
    function _git_branch_name
      echo (command git symbolic-ref HEAD 2> /dev/null | sed -e 's|^refs/heads/||')
    end
    
    function _git_is_dirty
      echo (command git status -s --ignore-submodules=dirty 2> /dev/null)
    end
    
    function fish_prompt
      set -l last_status $status
    
      set -l cyan (set_color cyan)
      set -l yellow (set_color yellow)
      set -l red (set_color red)
      set -l blue (set_color blue)
      set -l green (set_color green)
      set -l normal (set_color normal)
    
      set -l cwd $blue(pwd | sed "s:^$HOME:~:")
    
      # Add a newline before new prompts
      echo -e ''
    
      if set -q VIRTUAL_ENV
          echo -n -s (set_color -b cyan black) '[' (basename "$VIRTUAL_ENV") ']' $normal ' '
      end
    
      echo -n -s $cwd $normal
    
      if [ (_git_branch_name) ]
        set -l git_branch (_git_branch_name)
    
        if [ (_git_is_dirty) ]
          set git_info $green $git_branch $red " ✗" $normal
        else
          set git_info $green $git_branch ' ✔'
        end
        echo -n -s '  ' $git_info $normal
      end
    
      set -l prompt_color $red
      if test $last_status = 0
        set prompt_color $normal
      end
    
      echo -e ''
      echo -n -s "$normal▶ "
    end

    Neovim configuration

    I installed fish grammar for treesitter to get syntax highlighting in Neovim. I also had to install ripgrep from homebrew to get telescope live grep working.

    :TSinstall fish
    brew install ripgrep

    Thoughts

    I’ve been using fish for the past couple of days now, and I have to say that I like it alot. After configuring everything, I’ve noticed how fast the prompt appears compared to Oh My Zsh. I’ve also been really impressed with fish_config, which let’s you do some configuration from a web interface. This was really useful when deciding on colors for my prompt. I’ve also been liking the prevd and nextd commands which are mapped to alt + left/right arrow. I also really like that ctrl+c is shown after terminating stuff.

    If you want to copy my setup, feel free to check out my dotfiles!

  • 2 apr 2023 ~

    March 2023

    I got a new bike!

    my new bike

    Traveling

    Like February, I started the month by taking the train to Oslo. It was frigid there, but some days were sunny and dry, which made it possible to skate outside for the first time this year!

    nosemanual at nydalen phus

    After a week in Oslo, I flew out to Ericeira in Portugal, where I spent the week surfing with friends from Trondheim :)

    fishermans beach in ericeira

    Toward the end of the month, I got to attend the UX Copenhagen conference. I gained some practical knowledge on font selection and got to experience Copenhagen once again. It has grown to be one of my favorite cities!

    Reading

    I read two books this month, Slow Days, Fast Company: The World, The Flesh and LA by Eve Babitz, and Steve Jobs by Walter Isaacson. I’ve also started reading Bicycle Diaries by David Byrne, which I’m enjoying so far!

    Programming

    While in Oslo, I spent my days at the office working on Oslonøkkelen while jumping in and out of meetings for different school assignments. All of the context-switching can be tiring, but I’ve now finished this semester’s most extensive programming assignments, so I’m hoping things will quiet down a bit for April.

    Once again, I did a significant update of my personal website. I’ve sat on the domain isak.me since the start of 2023, and I finally got around to doing the switch. I plan on writing more about the updated website in a separate post later this month!

  • 30 mar 2023 ~

    Browser detection

    I’ve wanted to detect which browser is being opened when the user clicks on an external link in my app. I first tried using the applicationWillResignActive and application(_:open:options:) methods, but they are related to the application lifecycle and will not provide information about external interactions.

    One possibility might be to create an interceptor for link clicks, and then prompt the user with a selection of different browsers, but that does not seem like a smooth interaction for the user. Information about the default browser on the user’s device is hidden due to privacy restrictions in iOS.

  • 4 mar 2023 ~

    Surge

    While waiting for my coffee water to boil, I decided to try out surge.sh, a tool for static web publishing. After installing the cli using npm I ran the surge command in the directory I wanted to publish:

    surge

    And that’s what was needed to get the static content at isak.me published. After adding a CNAME record pointing to na-west1.surge.sh the site was up and running with a custom domain. The water was not even close to boiling yet!

    Their pro plan, priced at $30 per month, is needed to configure SSL for custom domains, so I probably won’t be using surge for anything in the foreseeable future, but it was still cool to see just how easy surge can host static content.

  • 2 mar 2023 ~

    February 2023

    My February started out on a train headed for Oslo. I spent a couple of days at the office and went on a skiing trip over the weekend which was really fun!

    Somewhere in Øvre Rendal

    skiing

    Reading

    I finished three books in February, Populæremusikk fra Vittula, Expeditionary Force: Columbus Day and Walking Through Clear Water in a Pool Painted Black.

    Programming

    February has been a month of starting new projects. I’ve started building the frontend for a and (maybe) improved Vengeful Vineyard. I’ve also spent quite a bit of time working on a group project at school building a simple CRUD React app. We have chosed the engine-backed “PU stack”, consisting of React + TypeScript, Tailwind and Firebase.

    In the iOS universe, I’ve learned about Universal Links which has been fun implementing while building a new feature for Oslonøkkelen.

    desk

    Somewhere in space

    A selfie taken by the student-built satellite SelfieSat. Getting this picture has been quite the bumpy road, and I’m amazed at how persistent Orbit NTNU has been at overcoming problems. Massive dub 🔥

    Somewhere in space

    selfie from space
  • 1 feb 2023 ~

    January 2023

    January has been a fun month and I thought I would write a recap of some of the things I’ve been up to!

    Reading

    To my surprise I ended up reading five books in January. Barbarian Days by William Finnegan, Klara and the Sun by Kazuo Ishiguro, 50/50 by Aslak Gurholt, Form+Code by Casey Reas and Never use Futura by Douglas Thomas. Barbarian Days was a blast to read and a clear favorite for this month. I also really liked Klara and the Sun. In general I’ve found a lot more time for reading which I have been enojoying a lot!

    Programming

    The semester at NTNU has had a really slow start which I’ve been completely fine with as it has given me plenty of time to spend working on other projects. In general I have found that school often comes in the way of doing actual development, so I’ve enojoyed having little school this month :) I’ve rolled out a couple of new features to production for Oslonøkkelen and I’ve started the build of a fun webapp at dotkom.

    In december I decided to give Neovim another try since my most recent attempt to switch last summer. The new setup has been coming along really well and I’ve started to favor it compared to my trusty VSCode setup.

    top of lille blåmann

    Traveling

    I spent the first week of January in Tromsø, which was great as I had plenty of time to go splitboarding.

    Taken from the top of Lille Blåmann

    the golden days

    Then I visited Åre with my friends from school. Snowboarding was fun but the nightlife was trash. We visited Bygget, one of the worst bars I’ve been to. The bouncers did drip-checks but only let lame fits inside :P

    Me and Magnus in Åre

    me and magg

    I also got to travel to Copenhagen with my friends Martin, Erik, Soley, Randi, Iselin and Agnes. It was super fun and I can’t wait to go back for UX Copenhagen in March!

    REPLIKA playing at Rust

    replika
  • 6 jan 2023 ~

    iOS Custom Button with Label

    GitHub contribution graph

    Implementing a custom button featuring a label, using Swift5 and UIKit 🕹

    Creating our button class

    We start by creating a new file, ButtonWithLabel. To keep our button customizable, we initialize our class with a type and an action.

    import UIKit
    
    class ButtonWithLabel: UIView {
        typealias Action = (() -> Void)?
        private var action: (() -> Void)?
    
        enum ButtonWithLabelType {
            case info
            case alert
        }
    
        private var type: ButtonWithLabelType
    
        init(type: ButtonWithLabelType) {
            self.type = type
            super.init(frame: .null)
       }
    
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        @discardableResult
        func with(action: Action) -> Self {
            self.action = action
            return self
        }
    
        @objc private func click() {
            action?()
        }
    }
    

    Creating the label and the icon

    Because we want the entire class to be pressable, we create both our label and our button as UIButton elements, and we add the previously created click function as the buttons click-targets. I’ve also added some simple styling to the label button, giving it some padding and a background color. We will style the button in the next step.

    private lazy var label: UIButton = {
        let button = UIButton()
        button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 6)
        button.backgroundColor = .white.withAlphaComponent(0.8)
        button.addTarget(self, action: #selector(click), for: .touchUpInside)
        return button
    }()
    
    private lazy var icon: UIButton = {
        let button = UIButton()
        button.addTarget(self, action: #selector(click), for: .touchUpInside)
        return button
    }()

    Setting the label text and styling the icon

    We want to create our label text and choose our icon based on the provided ButtonWithLabelType. To do this, we create a helper function, updateStyling(). In this example, I’ve used icons from SF Symbols.

    private func updateStyling() {
        switch type {
        case .info:
            label.setTitle("Info", for: .normal)
            icon.setImage(UIImage(systemName: "questionmark.circle"), for: .normal)
        case .alert:
            label.setTitle("Alert", for: .normal)
            icon.setImage(UIImage(systemName: "exclamationmark.circle"), for: .normal)
        }
    }

    Positioning

    We update our init function to call the newly created updateStyling. We also place our label and our icon in a UIStackView.

    init(type: ButtonWithLabelType) {
        self.type = type
        super.init(frame: .null)
        updateStyling()
    
        let stack = UIStackView(arrangedSubviews: [label, icon])
        stack.alignment = .center
    
        addSubview(stack)
        stack.fillInSuperview()
    }

    Usage

    We can now start using our new button by setting a type and giving it an action. In this example, I use it as a rightBarButtonItem.

    let infoButton = ButtonWithLabel(type: .info).with(action: showInfoView)
    navigationItem.rightBarButtonItem = UIBarButtonItem(customView: infoButton)

    And that’s it! Here is the completed class:

    import UIKit
    
    class ButtonWithLabel: UIView {
        typealias Action = (() -> Void)?
        private var action: (() -> Void)?
    
        enum ButtonWithLabelType {
            case info
            case alert
        }
    
        private var type: ButtonWithLabelType
    
        private lazy var label: UIButton = {
            let button = UIButton()
            button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 6)
            button.backgroundColor = .white.withAlphaComponent(0.8)
            button.addTarget(self, action: #selector(click), for: .touchUpInside)
            return button
        }()
    
        private lazy var icon: UIButton = {
            let button = UIButton()
            button.addTarget(self, action: #selector(click), for: .touchUpInside)
            return button
        }()
    
        init(type: ButtonWithLabelType) {
            self.type = type
            super.init(frame: .null)
            updateStyling()
    
            let stack = UIStackView(arrangedSubviews: [label, icon])
            stack.alignment = .center
    
            addSubview(stack)
            stack.fillInSuperview()
        }
    
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        private func updateStyling() {
            switch type {
            case .info:
                label.setTitle("Info", for: .normal)
                icon.setImage(UIImage(systemName: "questionmark.circle"), for: .normal)
            case .alert:
                label.setTitle("Alert", for: .normal)
                icon.setImage(UIImage(systemName: "exclamationmark.circle"), for: .normal)
            }
        }
    
        @discardableResult
        func with(action: Action) -> Self {
            self.action = action
            return self
        }
    
        @objc private func click() {
            action?()
        }
    }
  • 30 dec 2022 ~

    My 2022 In Programming

    GitHub contribution graph

    2022 is coming to an end, and I’ve wanted to write a summary of some of the programming projects I’ve worked on this year. Enjoy!

    SelfieSat 🛰

    image from selfiesat

    SelfieSat is a small CubeSat developed and built by OrbitNTNU. At the start of the year, I worked as an On-Board Computer Engineer on the SelfieSat project. I helped out programming the obc_deploy program, the first program that runs on the satellite after deployment. I also worked on a couple of smaller commands that the On-Board Computer would need to support.

    I developed software in C for the satellites’ custom (Linux-like) OS. The operating system was created by a previous student, Erlend. It is called ErlendOS, and to my knowledge, it is used nowhere else. This can cause tedious debugging sessions, especially for an inexperienced C developer like me. Still, overall it went well and has been an amazing project to be a part of!

    SelfieSat was launched on 25 May 2022, and we traveled to Cape Canaveral in Florida to see the launch live! The launch was hands down one of the most incredible things I have ever seen. It only got better once we established contact with the satellite on 29 August! Having written code that now floats around in space is bonkers to think about.

    We successfully took and transmitted the first image from SelfieSat on 6 December. Great way to end the year!

    Transporter 5

    Oslonøkkelen 💖

    Working on the app Oslonøkkelen has continued to be really fun. I continued working as a frontend developer, building and maintaining a CMS for the app throughout most of 2022. I’ve been working with the frontend stack of TypeScript, React, Redux and Sass. Working on a project that started a couple years back can sometimes be noticeable (code-wise). While the rest of the team took their summer vacation, I got the time to do some good old refactoring. Removing thousands of lines of code is always fun!

    In September, I switched from frontend to iOS development. Since the switch, I’ve learned a ton. It has been challenging, and I still feel lost sometimes, but overall I’m thrilled to work as an iOS developer. I’m enjoying Swift, and getting new features out to production is super fun. I sometimes miss working with the familiar web, but I’m excited to continue working with iOS in 2023.

    duckmouse.no 🐤

    My friend Gerhard and I bought 500 computer mice and have been selling them from our website, duckmouse.no. We have created the website using Gatsby and Tailwind, and a backend using Go and Stripe. Selling the mice has been great fun!

    Teknostart 🎤

    I got to hold a four-day git course with my friend Carl at the start of the fall semester at NTNU. We held a two-day lecture about git for all of the first-year students of Informatikk and Datateknologi. It was my first time lecturing, and I’m happy that I got the chance to try it out!

    School 🎒

    I’m in my second year at NTNU, studying for a bachelor’s degree in computer science. Throughout 2022 I’ve only had two courses that involve any programming. What’s really disappointing is that both of the programming courses focus on teaching JavaFX. I’m still not convinced that learning programming by attending university is engine backed.

    However, I had an enjoyable theoretical course, TDT4120 - Algorithms and Datastructures. The lecturer was great, and I found the curriculum very interesting. However, memorizing various algorithms and their use cases for an exam did not feel very productive, but I’m overall happy with the course.

    isak.me 🪴

    I keep rewriting this website, changing both its tech and design. During 2022 I have iterated through many implementations. I’ve tried out Jekyll, Hugo, Blot, and Next. I landed on Astro, which has been fantastic to work with. Without loading the images, the current size of this website is a mere 22KB. I am happy with how it has turned out, and I hope some time passes before I grow tired of this design as well.

    orbitntnu.com 🌐

    In February, I got the chance to switch from embedded development to creating a new web team for OrbitNTNU. Leading a small team of frontend developers was a great experience. We created a modern website using Gatsby, Tailwind, and Sanity.

    Other 🦮

    Those were the big ones, but I’ve worked on some more minor stuff as well:

    • I did a freelance project, building the landing page for Tycho Space Technologies.

    • I created a website for my friend Krister, but he has since scrapped my work and decided to build a new website from scratch all by himself, which is really cool!

    • My friend Leif and I created ov-bot, a Slackbot that checks whether Omega Verksted is open. Currently, while writing this post, it is closing in on over a thousand uses 🚂

    • Throughout the summer, I created a hand-drawn app using React Native. A very silent launch and a lack of advertising resulted in few users. I plan on working more on this next year.

    • I wanted to contribute more to Dotkom projects this year, without much success. But, I did get one contribution with this footer (which, according to the PR, took 7 months and 38 commits to create😅)

    2023

    2022 has been a great year, and I’m excited to continue my programming journey throughout 2023. A key takeaway from this year is that working on multiple projects at once is very challenging, especially when switching between languages and technologies. I often miss the days when I worked full-time and was immersed in just one project.

    My main goal for 2023 is to continue learning and improving as an iOS developer. I also want to find a web project that I can contribute to. Those are my priorities as of now, at least.

    And for a goal that is not programming related, I want to move abroad during 2023. I have already started looking at different possibilities, making me excited for the year to come.

    Happy New Year! 🎆

  • 30 dec 2022 ~

    Cannot find 'MenuBarExtra' in scope Fix

    MenuBarExtra is only available for macOS 13+. If you encounter the error above, make sure that your Xcode version is at least 14.1.

    import SwiftUI
    
    @main
    struct ScrollApp: App {
      var body: some Scene {
        MenuBarExtra("Menu Bar", systemImage: "circle") {
          Text("Hello, Menu Bar!")
          Image(systemName: "globe")
          Rectangle()
        }.menuBarExtraStyle(.menu)
      }
    }
  • 23 dec 2022 ~

    Implementing zoom controls for MapKit on iOS

    map with zoom buttons

    Buttons are usually much more accessible than gestures, so adding zoom buttons can be a great feature to add to any map. Unfortionaly, Apples MapKit does not support showsZoomControl for iOS, so we have to implement the controls ourselves.

    Buttons

    We start by creating two UIButtons. I’ll use icons from Apples SF Symbols (plus.magnifyingglass and minus.magnifyingglass).

    private lazy var increaseZoomButton: UIButton = {
        let button = UIButton(withAutoLayout: true)
        button.backgroundColor = R.color.color1()
        button.setImage(UIImage(systemName: "plus.magnifyingglass"), for: .normal)
        button.addTarget(self, action: #selector(increaseZoomLevel), for: .touchUpInside)
        button.layer.cornerRadius = 4
        button.layer.borderColor = R.color.color2()?.cgColor
        button.layer.borderWidth = 1
        return button
    }()
    
    private lazy var decreaseZoomButton: UIButton = {
        let button = UIButton(withAutoLayout: true)
        button.backgroundColor = R.color.color1()
        button.setImage(UIImage(systemName: "minus.magnifyingglass"), for: .normal)
        button.addTarget(self, action: #selector(decreaseZoomLevel), for: .touchUpInside)
        button.layer.cornerRadius = 4
        button.layer.borderColor = R.color.color2()?.cgColor
        button.layer.borderWidth = 1
        return button
    }()

    When pressed, the buttons call the functions increaseZoomLevel and decreaseZoomLevel. We will create those functions later.

    Positioning the buttons

    I use autoLayout to position the buttons after adding them to the view.

    view.addSubview(increaseZoomButton)
    view.addSubview(decreaseZoomButton)
    NSLayoutConstraint.activate([
        increaseZoomButton.topAnchor.constraint(equalTo: userLocation.bottomAnchor, constant: .normalSpacing(multiplier: 2)),
        increaseZoomButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -.normalSpacing(multiplier: 2)),
        increaseZoomButton.heightAnchor.constraint(equalToConstant: 44),
        increaseZoomButton.widthAnchor.constraint(equalToConstant: 44),
        decreaseZoomButton.topAnchor.constraint(equalTo: increaseZoomButton.bottomAnchor, constant: .normalSpacing(multiplier: 2)),
        decreaseZoomButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -.normalSpacing(multiplier: 2)),
        decreaseZoomButton.heightAnchor.constraint(equalToConstant: 44),
        decreaseZoomButton.widthAnchor.constraint(equalToConstant: 44),
    ])

    Handling button presses

    To handle the button presses, I have created two functions increaseZoomLevel and decreaseZoomLevel, which both call the zoomClickHandler function with their respective direction.

    @objc
    func increaseZoomLevel() {
        zoomClickHandler(direction: .increase)
    }
    
    @objc
    func decreaseZoomLevel() {
        zoomClickHandler(direction: .decrease)
    }

    zoomClickHandler

    The zoomClickHandler ensures that the user is sharing their location. It then calls the updateZoomLevel function, giving the user’s current location and the requested zoom direction as arguments.

    private func zoomClickHandler(direction: Direction) {
        if viewModel.location.isAuthorized {
            updateZoomLevel(location: mapView.userLocation.coordinate, direction: direction)
        } else {
            firstly {
                viewModel.location.requestAuthorization()
            }.then {
                self.viewModel.location.requestLocation()
            }.done { location in
                self.zoomTo(location: location.coordinate)
            }.catch { _ in
                self.coordinator.perform(action: .missingLocationSettings, in: self)
            }
        }
    }

    Updating the zoom level

    First, updateZoomLevel checks that the user’s current location is valid before proceeding. It then uses a helper function to get the mapView’s current location, which is then used to update the zoom level in the given direction.

    We then create a new viewRegion with the updated zoom level and use mapView.setRegion to apply the changes.

    private func updateZoomLevel(location: CLLocationCoordinate2D, direction: Direction) {
        guard CLLocationCoordinate2DIsValid(location),
            location.latitude != 0 && location.longitude != 0 else {
            return
        }
    
        let currentLocation = getCurrentMapLocation(mapView: mapView)
        let updatedLocation = getUpdatedMapLocation(currentLocation: currentLocation, direction: direction)
    
        let viewRegion = MKCoordinateRegion(center: mapView.region.center, latitudinalMeters: updatedLocation.metersInLatitude, longitudinalMeters: updatedLocation.metersInLongitude)
        mapView.setRegion(viewRegion, animated: true)
    }

    Helper functions

    To keep the controller clean, I have extracted a lot of the map logic to its own helper. This includes our Direction enum, a MapCoordinates struct, and the getCurrentMapLocation and getUpdatedMapLocation functions.

    import Foundation
    import MapKit
    
    enum Direction {
        case increase
        case decrease
    
        var bool: Bool {
            switch self {
            case .increase:
                return true
            default:
                return false
            }
        }
    }
    
    struct MapCoordinates {
        var metersInLatitude: Double
        var metersInLongitude: Double
    }
    
    func getCurrentMapLocation(mapView: MKMapView) -> MapCoordinates {
        let span = mapView.region.span
        let center = mapView.region.center
    
        let loc1 = CLLocation(latitude: center.latitude - span.latitudeDelta * 0.5, longitude: center.longitude)
        let loc2 = CLLocation(latitude: center.latitude + span.latitudeDelta * 0.5, longitude: center.longitude)
        let loc3 = CLLocation(latitude: center.latitude, longitude: center.longitude - span.longitudeDelta * 0.5)
        let loc4 = CLLocation(latitude: center.latitude, longitude: center.longitude + span.longitudeDelta * 0.5)
    
        return MapCoordinates(
            metersInLatitude: loc1.distance(from: loc2),
            metersInLongitude: loc3.distance(from: loc4)
        )
    }
    
    func getUpdatedMapLocation(currentLocation: MapCoordinates, direction: Direction) -> MapCoordinates {
        let updatedLatitude = currentLocation.metersInLatitude + (direction.bool ? (-currentLocation.metersInLatitude * 0.6) : (currentLocation.metersInLatitude * 2))
        let updatedLongitude = currentLocation.metersInLongitude + (direction.bool ? (-currentLocation.metersInLongitude * 0.6) : (currentLocation.metersInLongitude * 2))
    
        return MapCoordinates(
            metersInLatitude: updatedLatitude,
            metersInLongitude: updatedLongitude
        )
    }
  • 11 dec 2022 ~

    Using DigitalOcean Spaces for Image Storage

    Photo taken today from my window, now stored using Spaces 🌥️

    Top of church

    While building this site, I tried out Spaces by DigitalOcean to store photos. This tutorial will show you how I have everything set up.

    Motivation

    I wanted a place where I could store and share multiple high-res photos. Until now, I have stored the photos for this website locally next to the code, but this solution does not scale very well. Storing the photos elsewhere would free up space in my repository and allow me to use a CDN, which can increase the speed of serving static content.

    Spaces by DigitalOcean is a great alternative to AWS S3, as it supports the same API, has excellent documentation, and is reasonably priced. As a part of the GitHub Student Developer Pack, I have received $200 of credit to use on DigitalOcean, making it very tempting to try out.

    Domain registration

    I registered a new domain, isaks.cloud, which I plan on using for multiple DigitalOcean services. Setting up a bucket without a custom domain is also totally doable.

    I purchased my domain on namecheap, and swapped out their default nameserver settings to point to ns1.digitalocean.com. This allows DigitalOcean to manage the domain, which makes configuring the domain very easy.

    Changing nameservers

    Next up, in your DigitalOcean Cloud Dashboard, add your domain to your project from the Networking tab:

    Adding domain to digitalocean

    Creating a Space

    Hit the top Create button and choose Spaces. Then, select a datacenter region. I chose Amsterdam as it is the closest one to me.

    Creating a DigitalOcean Space

    CDN

    DigitalOcean makes it easy to enable a CDN for free. Hit Enable CDN and choose your custom domain (if you have one). Then, enter the subdomain you wish to enable, which in my case is photos.

    domain

    DigitalOcean will now generate an SSL certificate and set up a new CNAME record for your subdomain.

    Restrictions and name

    Leave File Listing Restrictions turned on, and select a unique name for your space.

    Setting Space Restrictions Setting a Space name

    Note: Including dots in the space name might result in https problem. This should not be problematic if you use your own domain.

    Uploading photos

    You can now start uploading images to your bucket, either through their UI or by using something like s3cmd.

    Uploading a photo

    And that’s it! You should now be able to access your image 🎉

    Image preview

    Conclusion

    I’m very happy with Spaces so far. If you want to try something similar, feel free to use my referal link to get $200 of credit, which you can use for two months.

    🌴

  • 9 dec 2022 ~

    This Website

    You are currently visiting isak.me, my very own domain on the internet. I bought this domain on 27 February 2019. Since then, I have iterated through many different builds, varying in both tech and design.

    As of today, 9 December 2022, the website is built using Astro. My experience using it thus far has been great. Astro’s “island architecture” allows me to write components using React, making the learning curve for Astro borderline non-existent. For styling and typography, I have added Tailwind and @tailwindcss/typography. The website is hosted using GitHub pages️.

    I have also added the open-source analytics tool Umami. It is currently running on Railway, but I am soon running out of the resources on their free starter plan, and I might end up scrapping the analytics together once the resources are used up.

    I am thrilled with how this version of my website has turned out. I do have some future improvements planned, with the main focus being on getting @astrojs/image working correctly. Once that has been implemented, I hope the stack of this website can stay static for a while.

    Update: I ended up scrapping my @astrojs/image implementation and decided to host my images using DigitalOcean Spaces.

  • 10 oct 2022 ~
  • 4 sept 2022 ~
  • Generating YouTube Titles Using Machine Learning

    Smoothbrain YouTube channel

    Last year I created a YouTube channel called Smoothbrain. I wanted a place where I could share some skateboarding clips with my friends, and I ended up posting many videos - that would sometimes go trending on YouTube Shorts. Since I created the channel, I have been consistently shitposting around one to three videos every day, which has resulted in over five-hundred video uploads at the point of writing this post.

    Because I have been posting so consistently, uploading the videos has become somewhat of a routine for me. The hardest part of creating a video is, of couse, coming up with a title. The titles I end up with are often complete trash, which made me wonder if I could write some code that could generate titles of an equal, or even better, standard.

    Getting Training Data

    Having already posted five-hundred videos, the way I chose to get training data was to use all of my existing YouTube titles. Manually writing down five-hundred video titles would be very tedious work, which is why I wrote a Go program that would do it for me.

    func getData(dataPointer *Data) {
    	URL := getUrl(dataPointer.NextPageToken)
    
    	resp, err := http.Get(URL)
    
    	if err != nil {
    		log.Fatalln(err)
    	}
    
    	defer resp.Body.Close()
    
    	setData(resp, dataPointer)
    }

    Using the YouTube API, I can send a request asking for the YouTube videos from the Smoothbrain channel. The API responds with a maximum of 50 videos, so I had to implement pagination in the requests.

    for {
    	for _, video := range data.Items {
    		formattedString := strings.ReplaceAll(video.Snippet.Title, " #shorts", "")
    		_, _ = datawriter.WriteString(formattedString + "\n")
    	}
    
    	getData(data)
    
    	if (data.NextPageToken == "") {
    		break
    	}
    }

    Running our Go-program gives us the file data.txt, which contains all of our titles.

    This guy is a poser 😎
    This guy is a chad 🥰
    Why does the filmer keep grunting
    This filmer needs to chill! 😎
    This is the fastest popshuvit I have ever seen 😎
    This fakie ollie goes hard 💥
    He almost had this trick 🔥
    ...

    Training Our Model

    I used a Python library called textgenrnn to easily train my own text-generating RNN on top of a pre-trained model.

    from textgenrnn import textgenrnn
    textgenrnn().train_from_file('../data/data.txt', num_epochs=10).generate()

    Generating Titles

    Having trained our model, we can generate titles using the same library.

    from textgenrnn import textgenrnn
    textgenrnn('textgenrnn_weights.hdf5').generate(30, temperature=1.0)

    Here are some examples of the results:

    Yo watch switch hes hard better to chad
    Sigma skating after beat straight
    Vitkig Kyle mid after this straight...
    Chad does switch flip this good?
    Yo which one manny or behind heelflip out boardida track
    Is Virgin Kyle dominantsey and this halfcab heel hard tailslide?
    Skate does fakie heelflip
    Dumb looking in the filmer part
    "Yo why do you counted this smood thich it"

    Uploading Some Videos

    Video uploads

    As we can see, the videos ended up getting some views. Not as much as I usually get, but not too bad either.

    Analytics graph

    Am I going to keep using the generated titles? Probably not. But it was fun checking out.

    Thanks for reading about this project! ⛄️

  • How to Add Firebase Analytics to a Gatsby Project

    With Gatsby being a Static Site Generator, adding functionality that uses browser globals can break the Node.js build step. This is the case for Firebase Analytics, so adding it to a Gatsby project can be a little tricky. In this article, I’ll show you how to add Firebase Analytics to a Gatsby project the correct way.

    Dependencies

    We will be adding three dependencies to our project to get Firebase Analytics up and running, dotenv, firebase and gatsby-plugin-firebase. When writing this article on March 31, 2022, gatsby-plugin-firebase only supports version 8.x.x of the firebase plugin.

    Using Yarn:

    yarn add dotenv firebase@8.10.0 gatsby-plugin-firebase

    Using NPM:

    npm install dotenv firebase@8.10.0 gatsby-plugin-firebase

    Firebase Setup

    Head over to console.firebase.google.com and create a new project if you haven’t already. Make sure you enable Analytics.

    Firebase landing page

    Next up, add Firebase as a web app. Once the app is set up, you can head over to the project’s settings to see the Firebase web configuration. You will need these values for the next step.

    Gatsby Setup

    We start by creating the file .env where we will be storing our environment variables. Remember to add this file to .gitignore if you don’t want your variables to be tracked by git.

    The .env should look like the following:

    GATSBY_FIREBASE_API_KEY=...
    GATSBY_FIREBASE_AUTH_DOMAIN=...
    GATSBY_FIREBASE_DATABASE_URL=...
    GATSBY_FIREBASE_PROJECT_ID=...
    GATSBY_FIREBASE_STORAGE_BUCKET=...
    GATSBY_FIREBASE_MESSAGING_SENDER_ID=...
    GATSBY_FIREBASE_APP_ID=...
    GATSBY_FIREBASE_MEASUREMENT_ID=...

    In your gatsby-config.js file:

    require("dotenv").config();
    
    module.exports = {
      plugins: [
        ...otherPlugins,
        {
          resolve: "gatsby-plugin-firebase",
          options: {
            features: {
              auth: false,
              database: false,
              firestore: false,
              storage: false,
              messaging: false,
              functions: false,
              performance: false,
              analytics: true,
            },
            credentials: {
              apiKey: process.env.GATSBY_FIREBASE_API_KEY,
              authDomain: process.env.GATSBY_FIREBASE_AUTH_DOMAIN,
              databaseURL: process.env.GATSBY_FIREBASE_DATABASE_URL,
              projectId: process.env.GATSBY_FIREBASE_PROJECT_ID,
              storageBucket: process.env.GATSBY_FIREBASE_STORAGE_BUCKET,
              messagingSenderId: process.env.GATSBY_FIREBASE_MESSAGING_SENDER_ID,
              appId: process.env.GATSBY_FIREBASE_APP_ID,
              measurementId: process.env.GATSBY_FIREBASE_MEASUREMENT_ID,
            },
          },
        },
      ],
    };

    Next up, add the following import to both gatsby-browser.js and gatsby-ssr.js:

    import "firebase/analytics";

    Usage

    Now that your Firebase Analytics is properly set up, you can start using it. This is an example of how you can track a page visit:

    import React, { useEffect } from "react";
    import firebase from "gatsby-plugin-firebase";
    
    const Home = () => {
      useEffect(() => {
        if (!firebase) {
          return;
        }
    
        firebase.analytics().logEvent("visited_homepage");
      }, [firebase]);
    
      return (
        <section>
          <h1>Hello, Firebase!</h1>
        </section>
      );
    };
    
    export default Home;

    When you run your website you should now see requests being sent to firebase.googleapis.com and google-analytics.com:

    Request sent to firebase.googleapis.com

    Request sent to google-analytics.com

    If the requests do not fail you have set everything up correctly! If you head back to console.firebase.google.com you should be able to check out the real-time analytics.

    Realtime analytics

    Restricting the API key

    It is a good idea to restrict your API key so that analytic requests can only be sent from a domain you own. To do this, head over to the Google Cloud Platform console at console.cloud.google.com. Select your project and navigate to the API Credentials page. Add an HTTP referrers restriction to match the URL of your website.

    restrictions

    You should now have Firebase Analytics set up on your Gatsby project! Thanks for checking out this article; any feedback is greatly appreciated!

  • 3 feb 2022 ~

    TailwindCSS in Storybook not working?

    If you are wondering why your Tailwind styling wont show up in Storybook, it might be because you have forgotten to import Tailwind. I might have forgotten to do this just now…

    At the top of .storybook/preview.js, add an import to your CSS file where you import Tailwind. In my case it would look like the following:

    import "../src/styles/global.css";

    And if you are using PostCSS 8+, remember to replace the PostCSS loader with the correct version using @storybook/addon-postcss.

  • 13 jan 2022 ~

    React Image onLoad

    In React, you can track the state of whether and image has loaded or not by passing the onLoad prop to an <img /> element.

    For my personal website isak.me, I have written the following Header.tsx component:

    const Header = () => {
      const [loaded, setLoaded] = useState(false);
    
      const handleOnLoad = () => setLoaded(true);
    
      return (
        <header>
          {!loaded && <EmptyContributionGraph />}
          <img
            src="https://ghchart.rshah.org/isaksolheim"
            className="contribution-graph"
            alt="Github contribution graph"
            onLoad={handleOnLoad}
          />
          <TextLink url="/" text="isak.me" />
        </header>
      );
    };

    I use the useState hook to add React state and keep track of the loaded value. Once the <img /> has loaded, the function handleOnLoad runs, which flips the loaded state from false to true. The temporary image <EmptyContributionGraph /> will not disappear.

    This is what the final component looks like:

    Example of onLoad

  • 13 jan 2022 ~

    Creating a Slackbot With JavaScript Bolt

    Creating a SlackBot that reacts to every message sent by a specific user.

    Bot reacting with heavy_plus_sign

    Creating a New Slack App

    The first thing we have to do is create a new slack app. We do this by navigating to api.slack.com and navigating to Your Apps -> Create New App.

    We’ll call our app reacting-bot. Next up, we have to set up event subscriptions, a bot user, and permissions.

    Socket Mode

    The first thing we have to do is enable Socket Mode. Turning on Socket Mode will route your app’s interactions and events over a WebSockets connection instead of sending these payloads to different Request URLs.

    To enable Socket Mode, navigate to Settings -> Socket Mode and hit Enable Socket Mode. You will be prompted to add scopes and generate an app-level token. Give the token a name, e.g., “token”, and add the following scopes:

    connections:write & authorizations:read

    As the names suggest, these will give our app permission to read and write messages.

    Event Subscriptions

    Next up, we need to enable Event Subscriptions, which can be found beneath Features in the left-side menu. After enabling Event Subscriptions, you will need to add the following Event Subscriptions.

    • Subscribe to bot events: message.groups
    • Subscribe to event on behalf of users: message.channels

    Bot User

    If a Bot User has not yet been created, it can be set up on the App Home tab. Here we can set a display name and its username.

    Permissions

    Lastly, we need to ensure that the SlackBot has all the correct permissions needed. Head over to OAuth & Permissions and add the reactions.write permission to the Bot Token Scopes.

    We have now set up the SlackBot, and we are ready to start coding!

    Writing The Bot

    We will be using the bolt-js library as an interface to the Slack API. It comes with many useful features that make creating SlackBots a breeze.

    While in a new directory reacting-bot, we can initialize the project using npm init -y. This will create a package.json file that looks like the following:

    {
      "name": "reacting-bot",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC"
    }

    For this project, we are going to need two packages. @slack/bolt and dotenv. Bolt is an interface that we use to communicate with the Slack API, and dotenv is used to access environment variables. We can install the packages with the following command:

    npm install @slack/bolt dotenv

    Next up, we need to create the file .env which should store our environment variables for when we are running this project locally. The file should contain the following:

    SIGNING_SECRET=5c24e...
    TOKEN=xoxb-...
    APP_TOKEN=xapp-...
    USER_ID=UCF8...
    • SIGNING_SECRET can be found on the Basic Information page
    • TOKEN (xoxb) can be found on the OAuth & Permissions page
    • APP_TOKEN (xapp) can be found on the Basic Information page under App-level tokens
    • USER_ID can be found when viewing a slack profile under More

    In index.js, we start by importing @slack/bolt and dotenv, and we initialize a bolt application in socket mode in the constant app:

    const { App } = require("@slack/bolt");
    require("dotenv").config();
    
    const app = new App({
      signingSecret: process.env.SIGNING_SECRET,
      token: process.env.TOKEN,
      socketMode: true,
      appToken: process.env.APP_TOKEN,
    });

    We then write the logic that uses the message() method to attach a listener for messages.

    app.message(async ({ message, context }) => {
      if (message.user === process.env.USER_ID) {
        try {
          const result = await app.client.reactions.add({
            token: context.botToken,
            name: "heavy_plus_sign",
            channel: message.channel,
            timestamp: message.ts,
          });
        } catch (error) {
          console.error(error);
        }
      }
    });

    We check any given message is sent by the user with the ID USER_ID. If we find a match, we use the app.client.reactions.add() function to add a reaction to the message.

    Lastly, we add a function that runs when the application starts. This function will start up the Slack App:

    (async () => {
      const port = process.env.PORT || 3000;
      await app.start(port);
      console.log(`⚡️ reacting-bot is running on port ${port}`);
    })();

    And that’s it! You can now run the app using the npm start command, and the bot will start reacting to messages once it has been added to the correct channels.

    This is what the final code looks like:

    const { App } = require("@slack/bolt");
    require("dotenv").config();
    
    const app = new App({
      signingSecret: process.env.SIGNING_SECRET,
      token: process.env.TOKEN,
      socketMode: true,
      appToken: process.env.APP_TOKEN,
    });
    
    app.message(async ({ message, context }) => {
      if (message.user === process.env.USER_ID) {
        try {
          const result = await app.client.reactions.add({
            token: context.botToken,
            name: "heavy_plus_sign",
            channel: message.channel,
            timestamp: message.ts,
          });
        } catch (error) {
          console.error(error);
        }
      }
    });
    
    (async () => {
      const port = process.env.PORT || 3000;
    
      await app.start(port);
    
      console.log(`⚡️ reacting-app is running on port ${port}`);
    })();

    Thanks for checking out this guide! You can view the code for this project on my GitHub.

  • 28 nov 2021 ~

    Switching from HTTPS to SSH in git repos

    This is a guide on how to resolve the following error:

    Username for 'https://github.com': USERNAME
    Password for 'https://USERNAME@github.com':
    remote: Support for password authentication was removed on August 13, 2021. Please use a personal access token instead.
    remote: Please see https://github.blog/2020-12-15-token-authentication-requirements-for-git-operations/ for more information.
    fatal: Authentication failed for 'https://github.com/USERNAME/REPO_NAME.git/'

    If you are getting this error, it might be because Github no longer accepts account passwords for HTTPS requests. You will need to either set up a PAT (Personal Access Token) or SSH keys. It is recommended to use SSH.

    Solution

    1: Set up SSH keys for your account. To do this, you can follow this guide.

    You will now need to update the local git config of the repository you are working on:

    2: Open .git/config in the project’s root directory.

    3: Change the url field from using the HTTPS protocol to using the SSH protocol.

    • (old https) https://www.github.com/USERNAME/REPO_NAME.git
    • (new ssh) git@github.com:USERNAME/REPO_NAME.git
  • 17 may 2021 ~

    How to Create an App Icon

    While working on my app MyVinyl, I eventually came to the point where I needed to start thinking about the icon. In this tutorial, I will share with you what I have learned and show you how to create an app icon. I will also show you how to set an icon for both iOS and Android projects. There are no prerequisites to follow this tutorial.

    Let’s start by considering some of the options you have when looking to create an app icon.

    Doing everything yourself

    Doing the entire design by yourself might work for some, but in my case, this was never an option. I don’t know a lot about icon design, and I would rather use the time it would take me to learn it on developing the app.

    The pros of doing it by yourself are that you have full creative freedom. Provided that you have an idea of what your icon should look like, and you know how to use a design tool, this might work for you.

    Outsourcing

    One option you have is outsourcing. You can use a service like Fiverr to find and pay someone to design and create an icon for you.

    Outsourcing is great because you can give designers the task of designing the app icon. A lot of services also have quick delivery, a couple of days at maximum, so getting your icon is relatively fast.

    One downside of outsourcing it is prone to communication errors. You have to be able to communicate your ideas about the icon, which can be hard, especially if you don’t have any well-defined ideas. You might end up paying $100+ on an icon that does not fit the app.

    Another downside is the price. The prices range from a couple of bucks to hundreds of dollars, depending on how many revisions you get and how much effort is going to be put into the icon. And the ones that only cost a couple of bucks usually do not include essential vector files, so a normal price would be 30–100USD.

    Initially, this was my plan for MyVinyl, but after looking into it I found another way that seemed better for me.

    My recommendation: Buying an icon

    This way is kind of like a hybrid between doing everything by yourself and outsourcing. There are plenty of services that sell icons for cheap, so the idea is that you find and buy an icon that you like, and turn that into an app icon. This might not work for you, but in my case, it seemed optimal.

    It is cheap, super quick, and super easy. The only downside I can think of is that you are limited by what icons already exists.

    Creating the icon

    The first step to creating the app icon is to find a starter icon that you like. There are plenty of places you can find icons, I recommend thenounproject.

    Searching for vinyl icons on thenounproject

    As you can see, there were 970 icons related to “vinyl”. The icons cost $2.99, so find one you like and buy it. Buying the icon will give you full access to use it commercially.

    Now that you have your icon, it is time to make it app-friendly. To do this you are going to need an application that supports editing vector graphics. Since I have Create Cloud, I will be using Adobe Illustrator. There are plenty of free options you can use as well. Sketch has a free trial you can use, or you can even use an online editor like Vectr.

    Create a new project with the width and height being 1024px.

    Setting the background color

    Start by setting a background color. A lot of apps have gradient backgrounds, so feel free to do it as well if that’s your style. Then import the vector graphic you just bought, and change its color if needed. Lastly, apply a simple shadow effect to make it “pop” out.

    Adding a shadow effect

    And just like that, the app icon is finished!

    Setting the icon

    Before you set the icon, you need to get the icon in a lot of different sizes. You can use an application or a website like easyappicon.com, it is free and works just like you would expect.

    Generating app icons

    Import your icon, remove any padding any hit download.

    Setting the icon for iOS

    In your (React) Native project, go to the following directory:

    ios/{projectName}/Images.xcassets

    Then, replace the existing AppIcon.appiconset folder with the one you downloaded.

    Replacing the appiconset folder

    If you open Xcode and navigate to the same directory, you can see that the icon has been applied with all the different sizes.

    Replacing the appiconset folder xcode

    Setting the icon for Android

    In your (React) Native project, navigate to the following directory:

    android/app/src/main/res

    Then, replace all the folders with the ones you downloaded.

    Replacing the res folder

    And there we have it. You have now created and set your brand new app icon. If you are interested in checking out MyVinyl, you can do it here. I hope you have found this tutorial useful. Any feedback is greatly appreciated!

  • 5 apr 2021 ~

    How To Use Relative Links With React Router

    In this tutorial, I’ll show you how to create a relative link with React Router. We will be writing the component EditButton which renders a button that appends /edit to the path the user is on. This will allow us to reuse the component all over our React project.

    import React from "react";
    import { Link } from "react-router-dom";
    
    const EditButton = () => (
      <Link to={´${window.location.pathname}/edit´}>
        <button>Edit</button>
      </Link>
    );
    
    export default EditButton;

    We start by writing the functional component EditButton. It has a button element wrapped around by the Link component from React Router.

    The link path is created by using window.location.pathname, which returns an initial '/' followed by the path of the URL as a string. Lastly, we add our /edit path.

    The resulting link looks like this: ´${window.location.pathname}/edit

    And there we have it. I hope you have found this useful.

  • 17 mar 2021 ~

    How To Create a Rotation Animation In React Native

    In this tutorial, I’ll show you how to create a button that rotates 720 degrees when it’s pressed. We will be using the React Native Animated animation system to create the rotation animation.

    Creating the component

    First, we create the functional component ButtonWithSpin that has a TouchableOpacity component with some text inside.

    return (
      <TouchableOpacity
        onPress={async () => handleAnimation()}
        style={{ width: 60 }}
      >
        <Animated.Text style={animatedStyle}>Click me</Animated.Text>
      </TouchableOpacity>
    );

    Notice that the text is created using the Animated.Text component. This is because the text that is going to rotate has to be animatable. If you are rotating something else, then you can use any of these components as well:

    • Animated.Image
    • Animated.ScrollView
    • Animated.View
    • Animated.Text
    • Animated.FlatList
    • Animated.SectionList

    We also use the useState Hook to store our rotateAnimation value. This is an animation value from the Animated library. We use `new Animated.Value(0) to initialize the value.

    The handleAnimation function

    const handleAnimation = () => {
      Animated.timing(rotateAnimation, {
        toValue: 1,
        duration: 800,
      }).start(() => {
        rotateAnimation.setValue(0);
      });
    };

    Animated.timing() animates a value over time using easing functions. By default, it uses the easeInOut curve which is perfect for what we are making. We pass our rotateAnimation value and a config value, where we set the duration to be 800ms. The animation is started by calling start(), and we include a callback function that resets the animation.

    Interpolate

    The interpolate() method maps input ranges to output ranges. A basic mapping to convert a 0–1 range to a 0–100 range would be:

    value.interpolate({
      inputRange: [0, 1],
      outputRange: [0, 100],
    });

    You can also use the `interpolate() method to map out strings. Since we want to rotate our text from 0 degrees to 720 degrees, our code will look like this:

    const interpolateRotating = rotateAnimation.interpolate({
      inputRange: [0, 1],
      outputRange: ["0deg", "720deg"],
    });
    
    const animatedStyle = {
      transform: [
        {
          rotate: interpolateRotating,
        },
      ],
    };

    We also created the animatedStyle object, which includes an array transform with the rotate value interpolateRotating that we just created.

    Putting everything together

    Add the animatedStyle object to the Animated.Text component and a width to the TouchableOpacity component and we are done!

    Button spinning after click

    import React, { useState } from "react";
    import { TouchableOpacity, Animated } from "react-native";
    
    const ButtonWithSpin = () => {
      const [rotateAnimation, setRotateAnimation] = useState(new Animated.Value(0));
    
      const handleAnimation = () => {
        Animated.timing(rotateAnimation, {
          toValue: 1,
          duration: 800,
        }).start(() => {
          rotateAnimation.setValue(0);
        });
      };
    
      const interpolateRotating = rotateAnimation.interpolate({
        inputRange: [0, 1],
        outputRange: ["0deg", "720deg"],
      });
    
      const animatedStyle = {
        transform: [
          {
            rotate: interpolateRotating,
          },
        ],
      };
    
      return (
        <TouchableOpacity
          onPress={async () => handleAnimation()}
          style={{ width: 60 }}
        >
          <Animated.Text style={animatedStyle}>Click me</Animated.Text>
        </TouchableOpacity>
      );
    };
    
    export default ButtonWithSpin;

    And there we have it. I hope you have found this useful.