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:
- Embed the view controller in a navigation controller
- Drag and drop a text-view on to the canvas; stretch and pin it to all edges
- Make its font 24
- 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… 🤦♂️ ) - Open the Assistant-editor and create a new outlet for the text-view called
textView
. - Create a new Cocoa Touch Class based on
UIViewController
and call it PreviewViewController.swift - 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.
- 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.
- 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
- 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. - Inside
viewDidLoad
add a call toNotificationCenter.default.addObserver
so that we get notified when a user connects a screen via theUIScreen.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)
- 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).
- Inside PreviewViewController
import Down
at the top of the file, then create an empty string variable with adidSet
property observer. Inside it create a constant of typeDown
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 calledattributedString
which will optionally try to call the methoddown.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 ouroutputView
to be ourattributedString
. The.attributedText
property represents the styled text displayed by the text view and is of typeNSAttributedString!
. For those like me who didn’t know it anNSAttributedString
is:A string that has associated attributes (such as visual style, hyperlinks, or accessibility data) for portions of its text.
- Inside ViewController.swift make this class conform to
UITextViewDelegate
- 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 aPreviewViewController
. If that succeeds setpreview.text = textView.text
.
If we build and run this it should work very nicely, albeit with a smallish font.
Adjust details
- 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.
- Inside ViewController.swift’s
viewDidLoad
we are going to add another observer: this time it will have aforName
parameter ofUIScreen.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. - 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
- In the storyboard, drag a split-view-controller on the screen. Erase everything except apart from the master view controller.
- Ctrl-drag from the master view controller to the navigation controller and choose “master view controller”
- Embed the Preview-view-controller into a navigation controller.
- Ctrl-drag from the master view controller to the navigation controller on the right and choose “detail view controller”
- Drag the little arrow in front of the Master View Controller.
- 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
- In AppDelegate.swift add class conformance to the
UISplitViewControllerDelegate
protocol. - 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 itsprefferedDisplayMode
be.allVisible
. Finally set the.maximumPrimaryColumnWidth
to be.greatestFiniteMagnitude
and thepreferredPrimaryColumnWidthFraction
=0.5
. - 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!