Swift on Sundays – Building a multi-screen Markdown renderer

This is Day66 for me and I will watch the recording from this last Sunday’s Paul stream. I need a bit of rest after that challenge.

I will not write too much here, simply a step by step remainder of what was done and why.

First steps

Fine, I am already stuck at the beginning because Paul is using something called Cocoapods which of course I have heard about but never understood what it was and how to use it.

Fortunately I found this website which explained to me how to install it. I opened a Terminal window and wrote inside:

sudo gem install cocoapods

That process completed in 22 seconds.

I then switched to the folder for my project using the cd command and wrote pod init followed by a return-key press and an open Podfile followed by a return-key press.

This opened the file called “Podfile” in TextEdit inside which I had to write “pod ‘Down’”. Would someone by grace tell me what is going on here?

So frustrating when people assume other people should know things!

After this we need to launch the pod install command which, on my computer, is taking ages to download things 20% after 150MB? What is this?

After a good ten minutes the receiving objects part was done, at which point the resolving deltas began. After another good five minutes it started checking out files after which it installed the desired components for our app.

That was long…

Now, typing ls we will get the list of the files in the folder and typing xed . we will open the Xcode workspace with “MultiMark” and “Pods” inside. From here:

  1. Embed the view controller in a navigation controller
  2. Drag and drop a text-view on to the canvas; stretch and pin it to all edges
  3. Make its font 24
  4. Ctrl-drag from that to the view controller yellow icon and select the delegate property (again, thank you for explaining us why this is so… 🤦‍♂️ )
  5. Open the Assistant-editor and create a new outlet for the text-view called textView.
  6. Create a new Cocoa Touch Class based on UIViewController and call it PreviewViewController.swift
  7. In the storyboard, drag out a new view controller, make it inherit from our newly created class and give it a Storyboard ID of “PreviewViewController” so that we can reference it back in code.
  8. Drag out a new text-view on to the new view controller and, this time, make it stretch from edge to edge of the screen as with external screens we will not have notches or times or whatever. Make this text-field non-editable by unchecking the “editable” property in the Attributes Inspector.
  9. Create an outlet from the new text view to the new view controller class. Call it outputView.

Make the two screens talk to each other

  1. Inside ViewController.swift create a new property of type [UIWindow]() so that we can store any amount of screens the user would connect to our device.
  2. Inside viewDidLoad add a call to NotificationCenter.default.addObserver so that we get notified when a user connects a screen via the UIScreen.didConnectNotification. Inside the closure write the following code:
// make sure that there is a view controller and that it is equal to itself
guard let self = self else { return }

// make sure that the object of the notification is a UIScreen            
guard let newScreen = notification.object as? UIScreen else { return }

// get its dimensions
let screenDimensions = newScreen.bounds
           
// make a new window based on those dimensions
let newWindow = UIWindow(frame: screenDimensions)
// set the screen on which this window is displayed to be the newScreen
newWindow.screen = newScreen
    
// instantiate the new view controller        
guard let vc = self.storyboard?.instantiateViewController(withIdentifier: "PreviewViewController") as? PreviewViewController else {
                fatalError("Unable to find PreviewViewController")
            }
     
// make the new view controller be the first view controller of the new window, set it visible and append it to the array of UIWindow we created before       
newWindow.rootViewController = vc
newWindow.isHidden = false
self.additionalWindows.append(newWindow)
  1. Try out that everything is working by building the app on an iPad simulator and then choosing Hardware > External Displays > (something sensible for the device).
  2. Inside PreviewViewController import Down at the top of the file, then create an empty string variable with a didSet property observer. Inside it create a constant of type Down with the initialiser (markdownString:) and our string variable as its only parameter. For you guys who are sick like I am with understanding things, here is the definition of that initialiser we just wrote:

    Initializes the container with a CommonMark Markdown string which can then be rendered depending on protocol conformance

    Then we set a new property called attributedString which will optionally try to call the method down.toAttributedString(). down is our previous property and the method does this:

    Generates an NSAttributedString from the markdownString property

    Finally we set the .attributedText property of our outputView to be our attributedString. The .attributedText property represents the styled text displayed by the text view and is of type NSAttributedString!. For those like me who didn’t know it an NSAttributedString is:

    A string that has associated attributes (such as visual style, hyperlinks, or accessibility data) for portions of its text.

  3. Inside ViewController.swift make this class conform to UITextViewDelegate
  4. At the bottom of the class add the textViewDidChange method and, inside it, check that the root view controller of the first element of the additional windows array is a PreviewViewController. If that succeeds set preview.text = textView.text.

If we build and run this it should work very nicely, albeit with a smallish font.

Adjust details

  1. Inside PreviewViewController.swift we add a property to the didSet observer using some sort of CSS (which, according to my primary school friend, Wikipedia, is an acronym for Cascading Style Sheets, a style sheet language used for describing the presentation of a document written in a markup language like HTML). Sometimes I would really like teachers to assume I am a complete idiot and just ELIF (explain like I’m five)… This property contains this string:
    We then add a parameter to the .toAttributedString call in the form of (stylesheet: style). This will make it work.

Manage disconnection

Now we have a problem because if we try another screen it will just not work because by connecting another screen we have destroyed the first element of the array so that the textViewDidChange method is actually talking to no one.

  1. Inside ViewController.swift’s viewDidLoad we are going to add another observer: this time it will have a forName parameter of UIScreen.didDisconnectNotification and, inside the closure, we will verify that there is a view controller and that it is the one we are writing into, then we will check that the old screen being disconnected is the object of the notification. If all this work we will check if the index of the old screen is the same as the first object of the additional windows array. If that works we will just remove it.
  2. We still have one issue which is that whenever we change screen the text is not ported. To solve this we need to call, at the end of the first “notification” bloc, self.textViewDidChange(self.textView).

Create support for split screen when an external screen is not connected

  1. In the storyboard, drag a split-view-controller on the screen. Erase everything except apart from the master view controller.
  2. Ctrl-drag from the master view controller to the navigation controller and choose “master view controller”
  3. Embed the Preview-view-controller into a navigation controller.
  4. Ctrl-drag from the master view controller to the navigation controller on the right and choose “detail view controller”
  5. Drag the little arrow in front of the Master View Controller.
  6. Inside ViewController.swift change the textViewDidChange to this:
func textViewDidChange(_ textView: UITextView) {
    if let preview = additionalWindows.first?.rootViewController as? PreviewViewController {
        preview.text = textView.text
    }
    
    if let navController = splitViewController?.viewControllers.last as? UINavigationController {
        if let preview = navController.topViewController as? PreviewViewController {
            preview.text = textView.text 
        }
    }
    
}

Solve the adaptive layout

  1. In AppDelegate.swift add class conformance to the UISplitViewControllerDelegate protocol.
  2. Inside the didFinishLaunchingWithOptions method make sure that the first view controller of the window is a split-view-controller. Make it the delegate of the split view controller, then makes its prefferedDisplayMode be .allVisible. Finally set the .maximumPrimaryColumnWidth to be .greatestFiniteMagnitude and the preferredPrimaryColumnWidthFraction = 0.5.
  3. Last but not least is to add the splitViewController(_:collapseSecondary:onto:) method and make it return true, which ensures it does nothing with the secondary view controller.

And that’s it!

You can find the code for this project here.

Please don’t forget to drop a hello and a thank you to Paul for all his great work (you can find him on Twitter) and don’t forget to visit the 100 Days Of Swift initiative page.


If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).

If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).

You can also support me by buying Paul Hudson’s books from this Affiliate Link.

Anyways, thank you so much for reading!

Till the next one!

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: