Training and Instructional Design
There are some powerful documentation features of Xcode you may not be using and will make your life a lot easier. Take a look at the downloaded exercise file. Go ahead and run it like I have here. I wrote a simple app to turn on and off a matrix of lights. Stop the app.
Looking at the ContentView code, It is hard to figure out what is going on. Click On LightbankView. I’ve got a light bank up top and a Toggle buttonView
below it that I made. One’s a model and one is a view. I might use elsewhere they are hard to add to the code.
Option-Click the VStack
. You get a popup that describes how a VSstack
works. If you open the right sidebar and click the Help icon you see the same info again. If you option-click LightBank
, you get very little, but you can change this. You see under the quick help Declared in. You can click that and go to the definition.
Above the definition, you’ll add a special comment. Instead of two slashes, add three. Add the comment.
/// A data structure for a row of lights
Hit Command-B to build the project. Click on the LightBank struct
and the quick help shows your summary.
Any time you add three slashes and text above a declaration, it shows up in the Quick Help summary. In Xcode 11, You can do this faster with a command-click. Command-click BankState
, and select Add documentation. You get a place marker for your summary. Add the following:
/// A pattern of lights.
As this is an enum
, I might want the possible values, so I’ll add that.
Can be .allOn, .allOff, .evenOn or .oddOn.
Really that should be in the discussion, not the summary. Press return before Can. The comment appears on the next line. Add a dash between the triple slash and Can. Build again and check the Quick Help. Both the sidebar and the popup show the values in the discussion section of the quick help.
/// - Can be .allOn, .allOff, .evenOn or .oddOn.
You can format this too. For using code case on my enum values, I’ll use the reverse single quote found above the tab key. I can quote my values like this:
/// - Can be `.allOn`, .`allOff`, `.evenOn` or `.oddOn`.
Build and check it out.
There are other callouts besides discussion. Head down to the init
. Command-click and Add documentation to init
. For methods with parameters you also get the parameters already listed for you. You can add these manually or use the Add parameter in Command-Click. Add these comments:
/// A data structure for a row of lights /// - Parameter count: The number of lights in a row /// - Parameter on: The pattern of lights in the row from `BankState`
I like having enum values handy, so I’ll finish this off with a copy and paste of the line above.
/// - Can be `.allOn`, .`allOff`, `.evenOn` or `.oddOn`.
Build again and head back to ContentView
.
Option-Click LightBank
. You get the summary for the Struct.
Option-Click on count and you get the quick help for the initializer.
Your summary even shows in the code completion. add this:
var moreLights = Lightban
Code completion has a summary.
You can do a lot more formatting and even links with markup in quick help. Markup is also used in Playgrounds. Check out chapter 4 of my Swift Playgrounds Application Development course for a lot more you can do with markup.
You can look at the completed code for this project on GitHub here:
In our last tip, I made a single checkbox in SwiftUI. Let’s learn more about collections in SwiftUI and build a checklist.
If you download the exercise file, you’ll find an expanded version of last week’s project. Under the model folder, I added some data for our list of pizzas to try.
let checkListData = [ CheckListItem(title: "Neopolitan"), CheckListItem(title: "New York"), CheckListItem(title:"Hawaiian"), CheckListItem(title:"Chicago Deep Dish"), CheckListItem(title:"Californian") ]
You’ll see that has a struct backing it that stores the title and an default value of false for each item.
struct CheckListItem{ var isChecked: Bool = false var title: String }
Suppose I wanted to make a list of all my items. Head to CheckListView
. I’d use a list like this:
List(checkListData){ item in CheckView(isChecked: item.isChecked, title: item.title) } .font(.title)
However we get a message that CheckListItem
does not conform to the Identifiable
protocol. Any object you iterate in Swift UI must have essentially a primary key, a unique identifier. I have two options here: The first is to use existing data, such as the title as my key. I can specify that by adding a parameter of id
, set to my title
.
List(checkListData, id:\.title){ item in
The error disappears. However, that means I cannot have two titles the same. If you have unique data this works fine. You have to specify the id
in this case, so this makes the checklist less adaptable.
Our second and better possibility is to adopt the protocol Identifiable
. Head over to the ChecklistItem
. Add the Identifiable
protocol to the struct.
struct CheckListItem:Identifiable{
This protocol has one required property named id
, which contains a unique identifier. While id
can be any hashable type, I’ll make id
an integer.
var id: Int
I’ll change my data to include the id
. I’ll add it to the first item. Then cut and paste into the rest.
let checkListData = [ CheckListItem(id:0,title: "Neopolitan"), CheckListItem(id:1,title: "New York"), CheckListItem(id:2,title:"Hawaiian"), CheckListItem(id:3,title:"Chicago Deep Dish"), CheckListItem(id:4,title:"Californian") ]
Our errors have disappeared.
Change your content view to use the CheckList view.
Again, I’ll Run on a device instead of the canvas. You get a nice looking checklist that you can check. This is not yet storing the checkmarks. We’ll look into that in another tip. You’ll find it is best to adopt the Identifiable
protocol for iterating in lists.
The code for this project is downloadable on GitHub at https://github.com/MakeAppPiePublishing/Tips_08_04_Checklist_End. There’s an accompanying video you can find at LinkedIn Learning.
If you’ve been working with View controllers for a while, you’ve probably dealt with prepareForSegue. In Xcode 11, there’s a new way to handle this that makes a little more sense: Segue actions. Let’s take a look at this feature.
If you download the storyboard, you’ll find I set one up for you. It is just a button that increments a counter on a detail view. Delete the segue between the two. I’ll use the button as navigation only. I’ll control-drag to the detail controller, and use a show segue.
Open up the assistant editor. Control drag from the segue to the code for ViewController somewhere below viewDidLoad
. Release the button. You’ll be asked for a connection. Name it addOneSegue. This method is a segue action, which triggers on a segue. It does much of what prepareForSegue
does without some of the overhead. Add the action, and close up the assistant editor. Head over to the View Controller code.
I’m going to make a counter. I’ll need a counter property here
var count = 1
You’ll be more specific here than in a prepareForSegue
. You make an instance of the controller, set its properties, and return the controller. No messing with id’s or anything else. Let’s try this.
@IBSegueAction func addOneSegue(_ coder: NSCoder) -> DetailViewController? {
First, I’ll make an instance. Because this is instantiating to coder, this is optional. So use an if let here.
if let detailVC = DetailViewController( coder: coder){
If the controller instantiates, I’ll add one and set that in the detail.
count += 1 detailVC.count = self.count
Then return the view controller.
return detailVC }
And finally, if it doesn’t work, return nil
.
return nil }
Over at the detail, we’re all set up with viewDidLoad
updating the count. Run this.
You’ll get the button. Tap it. You get a count. Hit Back, then the button. Again, it updates.
Segue actions save time in finding and naming segues and destination controllers. This new feature is a small gift of SwfitUI to UIKit. You’ll use this to get SwiftUI into storyboards, but I’ll discuss that at another time and in my Course SwiftUI Essential Training. Do remember outlet actions instantiate the instance, but do not load it. You still can’t assign a value to the label directly.
I find this a better alternative than prepare for segue in delegation between my view controllers.
You can find the code for this lesson in GitHub here:
Of the most underrated collection types is sets. Sets are an unordered collection with unique values. Let’s take a look at what you can do with sets.
Download the exercise files and you’ll find I created playground with a Struct called Pizza. I’ve made two pizzas for you. Currently, this is all arrays, but does it have to be arrays? Ask yourself three questions:
If you need ordered values and direct access, you’ll want an array. If you want unique values and don’t care about order or access, you’ll want a set. A Pizza is a good example of where a set works well. An ingredient is an ingredient. There is no good reason to list it twice, and it also has no particular order for toppings. I don’t need to access only one ingredient. That’s a set. Sets are stored and accessed more eifficiently than array or dictionaries, so if you can use a set, do so.
Lets convert this struct to sets. For the two constants on top, I add Set and the compiler will implicitly make this a set of strings. For cheese and toppings, I have to get more explicit.
var cheeses: Set<String>
var toppings: Set<String>
I added the type of the set in angle brackets. Run what you got and it still works. The pizzas below still compile.
You can enumerate a set using for. For example:
for cheese in quattroFormaggi.cheeses{
print(cheese)
}
You are not guaranteed the order however. If I want them sorted, I can use the sorted method
for cheese in quattroFormaggi.cheeses.sorted(){
print(cheese)
}
The core of sets is membership testing. I can check for the existence of something easily.
chicago.toppings.contains("Sausage")
returns true
quattroFormaggi.toppings.contains("Sausage")
Return false.
I can look for a bigger set too. I have a set of toppings here. I can check it against one of my pizzas.
margheritaDoc.toppings.isSubset(of: toppings)
That comes out false. I can find the missing ingredients quickly.
margheritaDoc.toppings.subtracting(toppings)
And I find I have whole tomatoes instead of crushed here.
Are any authentic cheeses also toppings? I can use an intersection to check that.
margheritaDoc.toppings.intersection(margheritaDoc.authenticCheese)
That gives me one case. If I want true or false I can use isEmpty. To look for the empty set.
margheritaDoc.toppings.intersection(margheritaDoc.authenticCheese).isEmpty
What if I want to check against two pizzas. I can union the pizzas and check that against the list.
let cheeses = quattroFormaggi.cheeses.union(margheritaDoc.cheeses)
cheeses.intersection(quattroFormaggi.authenticCheese)
So What of the two lists are excluded?
cheeses.symmetricDifference(quattroFormaggi.authenticCheese)
I for one would consider all that Authentic.
In this simple example you can see that if you don’t care about order and have unique values, Sets may be a an efficient way of storing data..
You can download this Swift playground from GitHub here: Tips_08_01_Sets_End
struct Pizza{ let authenticCheese: Set = ["Bufalo","Fior de late","Gorgonzola","Mozzarella","Parmigiano"] let authenticIngredients: Set = ["Basil","Peppers","Tomatoes", "Basil", "Oregano"] enum Crust{ case thin,thick,pan,lavosh,potPie } var crust : Crust var cheeses: Set<String> var toppings: Set<String> } let margherita = Pizza(crust: .thin, cheeses: ["Mozzarella"], toppings: ["Basil","Tomatoes","Parmigiano","Oil"]) let margheritaDoc = Pizza(crust: .thin, cheeses: ["Bufala"], toppings: ["Basil","Tomatoes","Parmigiano","Oil"]) let chicago = Pizza(crust: .pan, cheeses: ["Mozzarella"], toppings: ["Pizza Sauce","Sausage"]) let quattroFormaggi = Pizza(crust: .thin, cheeses: ["Fontina","Gorgonzola","Mozzarella","Parmigiano"], toppings: ["Crushed Tomatoes","Basil","Oil"]) for cheese in quattroFormaggi.cheeses.sorted(){ print(cheese) } let toppings: Set = ["Crushed Tomatoes","Basil","Oil","Chicken","BBQ Sauce","Red Onions","Parmigiano", "Peppernoi","Prociutto","Pineapple","Canadian Bacon"] chicago.toppings.contains("Sausage") quattroFormaggi.toppings.contains("Sausage") margheritaDoc.toppings.isSubset(of: toppings) margheritaDoc.toppings.subtracting(toppings) margheritaDoc.toppings.intersection(margheritaDoc.authenticCheese).isEmpty let cheeses = quattroFormaggi.cheeses.union(margheritaDoc.cheeses) cheeses.intersection(quattroFormaggi.authenticCheese) cheeses.symmetricDifference(quattroFormaggi.authenticCheese)
A few tips ago, we went under the hood with unicode Characters and their relationship to the Swift String
type. For most, that’s great theory, but how does it apply to strings, not characters? When I started programming back in the 1980’s, I had three string functions in BASIC: RIGHT$
, LEFT$
, and MID$
. Let’s create a simple extension to String that will let you use MID$
using string ranges, then discuss right$ and left$ equivalents, and how these get returned.
Download the exercise files. head to the embedded playground and hide everything but the playground. set run to manual if necessary.
You’ll find I started an extension.
extension String{ }
I also added an example string you run the playground, you’ll find the string has a lot of grapheme clusters of arrows and emoji.
Strings in Swift do not use integer string indexes to access characters due to grapheme clusters. Instead of arrays, strings are an ordered collection that uses relative distances from two constants, startIndex
and endIndex
.
For my function I’ll need those string indexes. I made you a function to get those, returning an optional value. If we are out of range, it returns nil.
func index(_ position:Int)->String.Index!{ if position < 0 || position > self.count {return nil} return self.index(self.startIndex, offsetBy: position) }
For my midString
method, I want a string starting at one character position and going for a length,
func midString(from index:Int, length:Int)->String!{ }
First I’ll check if my starting positions is a valid one, returning nil
if not a valid position:
if let startPosition = self.index(index){ } return nil
String Indexes take ranges, so I use a closed range from the startPostion
to an endPosition
. I calcualte the endPosition
by adding the length, but still have to take 1 off the length. That’s an optional so I’ll optionally chain it this way.
if let endPosition = self.index(index + length - 1){ }
Then I should be able to return the range.
return self[startPosition...endPosition]
Except that gets me an error. When you take something from a String range, you don’t get a string, but a substring. Substrings are excellent for memory allocation but extremely unstable, since they are really a set of pointers to the sub string within the parent string. As soon as possible with a substring, instantiate it as a string.
return String(self[startPosition...endPosition])
Now to test all this with a string with extended grapheme clusters
print(yummy.midString(from: 3, length: 3))
And I get
I’ll change this to an invalid length
print(yummy.midString(from: 3, length: 35))
Which returns nil
. That works as we wanted.
Ranges work in strings to get you substrings. If you want the beginning or end of a string, you use prefix
and suffix
, which also return substrings.
I can use
print(yummy.prefix(3))
to get the substring
Or I can use
print(yummy.suffix(3)
And get the substring.
If I overflow these,
yummy.suffix(35) yummy.prefix(35)
I get the full string, but as a substring. All Of what I did here is on grapheme clusters. Try one with both
yummy.suffix(5).prefix(1)
returns
There are other ways of breaking apart a string if you want the diacritical marks to be separate from the associated glyph, but for most applications, this is all you need.
Here’s the code for the playground If you don’t want to download it from GitHub.
var yummy = "D\u{1f369}ugh\u{20d7}n\u{20ed}uts" extension String{ func index(_ position:Int)->String.Index!{ if position < 0 || position > self.count {return nil} return self.index(self.startIndex, offsetBy: position) } func midString(from index:Int, length:Int)->String!{ if let startPosition = self.index(index){ if let endPosition = self.index(index + length - 1){ return String(self[startPosition...endPosition]) } } return nil } } //Now to test all this with a string with extended grapheme cluster yummy yummy.midString(from: 3, length: 3) yummy.midString(from: 3, length: 35) yummy.prefix(3) yummy.suffix(3) yummy.prefix(35) yummy.prefix(35) yummy.suffix(5).prefix(1)
Your app can get into problems when you launch UI from others threads, such as closures.
For example, you might have an app that going to ask for permissions for things like photos, notifications, or location data. The system usually handles those, but you might want to be even more exclusive on what to use, and use your own alert. In this tip, I’ll show you how to launch an alert from a closure.
I’ll use an slightly modified exercise file from the iOS and watchOS App Development:Notifications course I have in the LinkedIn Learning library. If you haven’t seen the course yet, notifications need permissions, and in that exercise file I show how to set up permissions. Since the demo app requires notifications, if the user prohibits notifications, I want the app to tell them that’s a problem.
I’ll use this method I wrote in an earlier tip to launch the settings
let settingsAction = UIAlertAction(title: "Settings", style: .default) { (action) in if let appSettings = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(appSettings, options: [:], completionHandler: nil) } }
I’ll use this alert in my user permissions. I have two actions in my app that send notifications. Before they do, they check if they are allowed to using the getNotificationSettings method. That method runs a closure. In that closure, add the code to run the alert if denied or not determined on schedulePizza
and makePizza
UIActions.
if status == .denied || status == .notDetermined{ self.accessDeniedAlert() return }
I’ll erase content and settings on my iPhone XR simulator to have a clean slate for permissions. Once it is ready, I’ll run the app.
I’ll get the permissions alert from the system. I’ll hit Don’t allow, and try to schedule a pizza. We get the alert.
However, look at the console in Xcode.
2019-06-19 06:42:58.245095-0500 Huli Pizza Notification[14948:369926] [Assert] Cannot be called with asCopy = NO on non-main thread.
You have an error message, one I would not ignore.
The app is running a UI thread of an alert on a non UI thread. The closure is creates a new thread, but alerts really need to be on the main thread where all the User interface is. There’s a huge problem with this that might not be easy to track if you are not aware of it. Because this alert is not running on the main thread, the main thread barrels on, running code without the correct settings. You can get very confused tracking down what happened with that.
You must run UI on the main thread so the rest of the system responds correctly. Fortunately that is easy. Just state what code you want running on the main thread. You can assign something to the main thread with he main singleton of the DispatchQueue
class. I’ll change the code for a denied permission in schedulePizza
and makePizza
to this:
if status == .denied || status == .notDetermined{ DispatchQueue.main.async { self.accessDeniedAlert() } return }
Clean and Run again, and now we don’t get the error message. I’m using an example from a notification, but anywhere you plan to call an object on the main thread, such as presenting an alert or changing a label, but doing so from a closure, make sure you assign the object to the main thread before doing so.
This week’s project has a big one backing it, so I’m only going to show you the view controller here. Download the project from Github for the full project.
// // ViewController.swift // Huli Pizza Notification // // Created by Steven Lipton on 11/23/18. // Copyright © 2018 Steven Lipton. All rights reserved. // import UIKit import UserNotifications // a global constant let pizzaSteps = ["Make pizza", "Roll Dough", "Add Sauce", "Add Cheese", "Add Ingredients", "Bake", "Done"] class ViewController: UIViewController { var counter = 0 @IBAction func schedulePizza(_ sender: UIButton) { UNUserNotificationCenter.current().getNotificationSettings { (settings) in let status = settings.authorizationStatus if status == .denied || status == .notDetermined{ DispatchQueue.main.async { self.accessDeniedAlert() } return } self.introNotification() } } @IBAction func makePizza(_ sender: UIButton) { UNUserNotificationCenter.current().getNotificationSettings { (settings) in let status = settings.authorizationStatus if status == .denied || status == .notDetermined{ DispatchQueue.main.async { self.accessDeniedAlert() } return } self.introNotification() } } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. navigationController?.isNavigationBarHidden = true } //MARK: - Support Methods // A function to print errors to the console func printError(_ error:Error?,location:String){ if let error = error{ print("Error: \(error.localizedDescription) in \(location)") } } //A sample local notification for testing func introNotification(){ // a Quick local notification. let time = 15.0 counter += 1 //Content let notifcationContent = UNMutableNotificationContent() notifcationContent.title = "Hello, Pizza!!" notifcationContent.body = "Just a message to test permissions \(counter)" notifcationContent.badge = counter as NSNumber //Trigger let trigger = UNTimeIntervalNotificationTrigger(timeInterval: time, repeats: false) //Request let request = UNNotificationRequest(identifier: "intro", content: notifcationContent, trigger: trigger) //Schedule UNUserNotificationCenter.current().add(request) { (error) in self.printError(error, location: "Add introNotification") } } //An alert to indicate that the user has not granted permission for notification delivery. func accessDeniedAlert(){ // presents an alert when access is denied for notifications on startup. give the user two choices to dismiss the alert and to go to settings to change thier permissions. let alert = UIAlertController(title: "Huli Pizza", message: "Huli Pizza needs notifications to work properly, but they are currently turned off. Turn them on in settings.", preferredStyle: .alert) let okayAction = UIAlertAction(title: "Dismiss", style: .default, handler: nil) let settingsAction = UIAlertAction(title: "Settings", style: .default) { (action) in if let appSettings = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(appSettings, options: [:], completionHandler: nil) } } alert.addAction(okayAction) alert.addAction(settingsAction) present(alert, animated: true) { } } }
One dilemma you’ll find when working with colors is switching between color systems. There’s two you’ll most often be using: the Red-Green-Blue or RGB and Hue-Saturation-Brightness or HSB. Download the Exercise file and run. It will give you the HSB value, but what if you want a RGB Value for that color? What if you want the values for any UIColor
? Let’s look at some special methods for that.
In the exercise files, go to ColorModel.swift
and the ColorEntry
class. I’ll add some methods here to do all this.
I stubbed a method rgbString
here for you with a few variables:
func rgbString()->String{ var rgb = "" var red:CGFloat = 0 var green:CGFloat = 0 var blue:CGFloat = 0 var alpha:CGFloat = 0 return rgb
There’s a UIColor
method getRed
that gets you the red green, blue and alpha components of a color. It works a bit differently than most methods though. Its parameters are unsafe mutable pointers, and it returns a Bool
if it was successful. I’ll put it in an if
statement to catch any errors like this:
if !color.getRed(&red, green: &green, blue: &blue, alpha: &alpha){ }
Unsafe pointers work a little backward. You put a strong pass-through variable for the parameter prefixed by the ampersand (&). I invert the bool here so I’ll deal with error conditions in the if
clause. I’ll just return the blank rgb value in this case, which
if !color.getRed(&red, green: &green, blue: &blue, alpha: &alpha){
return rgb
}
If successful, I’ll make a string to return the RGB color as a String
.
rgb = String(format:"R:%04.3f G:%04.3f B:%04.3f",red,green,blue)
I can do the same with HSB Colors wit the getHue
method. I already have properties for this in my model, I just need to add the alpha. The code for this is very similar.
func hsbString()->String{ var hsb = "" var alpha:CGFloat = 0 if !color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha){ return hsb } hsb = String(format:"H:%04.3f S:%04.3f B:%04.3f",hue,saturation,brightness) return hsb }
I can now set my color name by these two strings. I’ll make an initializer that creates the name from the strings:
init(color:UIColor){ self.color = color self.name = hsbString() + "\n" + rgbString() }
I’ll also change the name on my first initializer to include both:
init(name:String,color:UIColor){ self.color = color self.name = name + "\n" + hsbString() + "\n" + rgbString() }
I don’t need to use the first initializer in the hues method of color model, I can use the second, so I’ll change that:
//let name = String(format:"H:%04.3f S:1.0 B:1.0 ",hueValue) let colorEntry = ColorEntry(color: color)
Run the app. You’ll see the table has changed. When you select a color, both values show up.
You can use these methods for testing colors in many places, including pixels in images. While a little clunky with he unsafe mutable pointers, this is a relatively easy way to get get colors from one system to another.
I’ve been using the same code for the last few weeks. You can go to the last lesson to get everything but the changes made to ColorModel.swift, which is below. You can also download the completed project from Github.
// // ColorModel.swift // ColorPicker // // // An exercise file for iOS Development Tips Weekly // by Steven Lipton (C)2018, All rights reserved // For videos go to http://bit.ly/TipsLinkedInLearning // For code go to http://bit.ly/AppPieGithub // import UIKit class ColorEntry{ var name:String = "" var color:UIColor var hue:CGFloat = 0.0 var brightness:CGFloat = 0.5 var saturation:CGFloat = 1.0 init(name:String,color:UIColor){ self.color = color self.name = name + "\n" + hsbString() + "\n" + rgbString() } init(color:UIColor){ self.color = color self.name = hsbString() + "\n" + rgbString() } func rgbString()->String{ var rgb = "" var red:CGFloat = 0 var green:CGFloat = 0 var blue:CGFloat = 0 var alpha:CGFloat = 0 if !color.getRed(&red, green: &green, blue: &blue, alpha: &alpha){ return rgb } rgb = String(format:"R:%04.3f G:%04.3f B:%04.3f",red,green,blue) return rgb } func hsbString()->String{ var hsb = "" var alpha:CGFloat = 0 if !color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha){ return hsb } hsb = String(format:"H:%04.3f S:%04.3f B:%04.3f",hue,saturation,brightness) return hsb } } class ColorModel{ var colors = [ColorEntry]() init(){ colors = [] } func hues(count:Int)->[ColorEntry]{ colors = [] if count <= 0 {return colors} for hue in 0...count{ let hueValue = CGFloat(hue)/CGFloat(count) let color = UIColor(hue: hueValue, saturation: 1.0, brightness: 1.0, alpha: 1.0) //let name = String(format:"H:%04.3f S:1.0 B:1.0 ",hueValue) let colorEntry = ColorEntry(color: color) colors += [colorEntry] } return colors } func lightnessScale(hue:UIColor,count:Int){ } }
For reading ease and visual accessibility you should be using dynamic fonts whenever possible. Let’s look at an example you can get from the download files. I’ve run the Split View Controller from a previous tip in landscape on an iPad Pro 9.7 inch. Take a look at the labels on the table.
They are hard to read because they are so small. iOS has a system to make fonts bigger using dynamic type. Let’s look at a few issues with dynamic type programmatically.
For the user, they need to change one setting to make this bigger. ON the iPad simulator, click the Home button, and Settings. Select General>Accessibility,
Then Larger Text
On the bottom is a slider which you can notch up for larger text. For even larger text you can click on the switch on top. Make this pretty big.
That scales up the text in settings too. Go back to the app. You see bigger sizes but with white backgrounds,
The simulator has problems with this adjustment, which is why the white backgrounds. . Stop the app and re-run it.
By default, cells are dynamic text, the type called Body. Now rotate the simulator with a Command Left-Arrow, and drag out the master. Depending on the device, you may see the text wrap or get cut off.
To make sure it wraps, Head to the HueColorTableViewController
and the cellForRowAtIndexpath
method. Add the following line:
cell?.textLabel?.numberOfLines = 0
Setting the label property numberOfLines
to 0
does two things: it allows for a variable number of lines and activates a word wrap mode. Run again, and if you didn’t before, you’ll see our text wrap in the cells.
I still have a static font for my detail label. Head over there and You’ll see I have a UIFont
of 30-point American Typewriter. For dynamic type, You have two choices here. One is to use one of the system dynamic fonts. Add this to your code:
let dynamicFont = UIFont.preferredFont(forTextStyle: .title1)
The preferred Font method sets the font to the dynamic font title1
. I’ll change the font to the dynamic font:
colorLabel.font = dynamicFont
Run this
. This time, The font is a lot more visible, but on the system font. If I wanted a custom font, I’d have to do a few more steps. I’ll assign the colorLabel's
font to a class UIFontMetrics
and its default,
colorLabel.font = UIFontMetrics.default.scaledFont(for: font)
Finally, I need to tell the label to scale the font.
colorLabel.adjustsFontForContentSizeCategory = true
To be sure we get word wrapping, I’ll do this again
colorLabel.numberOfLines = 0
Run this. Change the font size to the smallest.
You’ll see the label remains at 30 point, as the Master dynamic type in the cells shrink.
Now make the font as large as it can get and run again. The font scales.
There is a complete course in the course library on accessibility which deep dives this topic and shows you the storyboard version of all this. It is something you should check out for better accessibility and adaptability of your user interface.
Here’s the code for this week’s project. You can download the full project at Github.
// // HueColorTableViewController.swift // ColorPicker // // // An exercise file for iOS Development Tips Weekly // by Steven Lipton (C)2018, All rights reserved // For videos go to http://bit.ly/TipsLinkedInLearning // For code go to http://bit.ly/AppPieGithub // import UIKit protocol HueColorTableViewControllerDelegate{ func didSelectColor(color:ColorEntry) } class HueColorTableViewController:UITableViewController{ var delegate:HueColorTableViewControllerDelegate! = nil var colors:[ColorEntry] = ColorModel().hues(count: 12) override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return colors.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { var cell = tableView.dequeueReusableCell(withIdentifier: "cell") if cell == nil { cell = UITableViewCell(style: .default, reuseIdentifier: "cell") } let row = indexPath.row cell?.contentView.backgroundColor = colors[row].color cell?.textLabel?.text = colors[row].name cell?.textLabel?.numberOfLines = 0 return cell! } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let row = indexPath.row title = colors[row].name delegate.didSelectColor(color: colors[row]) } override func loadView() { super.loadView() } }
// // ColorDetailViewController.swift // ColorPicker // // An exercise file for iOS Development Tips Weekly // by Steven Lipton (C)2018, All rights reserved // For videos go to http://bit.ly/TipsLinkedInLearning // For code go to http://bit.ly/AppPieGithub // import UIKit class ColorDetailViewController: UIViewController, HueColorTableViewControllerDelegate { let colorLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() addLayout() // Do any additional setup after loading the view. } //MARK:- Delegates func didSelectColor(color: ColorEntry) { view.backgroundColor = color.color colorLabel.text = color.name } //MARK:- Layout // All layout methods go here. func addLayout(){ colorLabel.text = "Color Detail" let font = UIFont(name: "AmericanTypewriter", size: 30)! //let dynamicFont = UIFont.preferredFont(forTextStyle: .title1) colorLabel.font = UIFontMetrics.default.scaledFont(for: font) colorLabel.adjustsFontForContentSizeCategory = true colorLabel.numberOfLines = 0 colorLabel.backgroundColor = .lightGray colorLabel.textAlignment = .center colorLabel.numberOfLines = 0 view.addSubview(colorLabel) colorLabel.translatesAutoresizingMaskIntoConstraints = false var constraints = [NSLayoutConstraint]() constraints += [NSLayoutConstraint(item: colorLabel, attribute: .leading, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .leading, multiplier: 1.0, constant: 0)] constraints += [NSLayoutConstraint(item: colorLabel, attribute: .trailing, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .trailing, multiplier: 1.0, constant: 0)] constraints += [NSLayoutConstraint(item: colorLabel, attribute: .top, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .top, multiplier: 1.0, constant: 0)] constraints += [NSLayoutConstraint(item: colorLabel, attribute: .height, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .height, multiplier: 1 / 9, constant: 0)] view.addConstraints(constraints) } }
// // ColorModel.swift // ColorPicker // // // An exercise file for iOS Development Tips Weekly // by Steven Lipton (C)2018, All rights reserved // For videos go to http://bit.ly/TipsLinkedInLearning // For code go to http://bit.ly/AppPieGithub // import UIKit class ColorEntry{ var name:String = "" var color:UIColor var hue:CGFloat = 0.0 var brightness:CGFloat = 0.5 var saturation:CGFloat = 1.0 init(name:String,color:UIColor){ self.color = color self.name = name } } class ColorModel{ var colors = [ColorEntry]() init(){ colors = [] } func hues(count:Int)->[ColorEntry]{ colors = [] if count <= 0 {return colors} for hue in 0...count{ let hueValue = CGFloat(hue)/CGFloat(count) let color = UIColor(hue: hueValue, saturation: 1.0, brightness: 1.0, alpha: 1.0) let name = String(format:"H:%04.3f S:1.0 B:1.0 ",hueValue) let colorEntry = ColorEntry(name: name, color: color) colors += [colorEntry] } return colors } func lightnessScale(hue:UIColor,count:Int){ } }
An often ignored but rather powerful View Controller is the UISplitViewController
. You can make one from a template and the storyboard, but I often skip both and do it programmatically, which is especially good when I’m prototyping in a playground. Let’s give it a try using the exercise files I’ve been using for the past few tips. There we added a color table. Split view controllers have a master, which I’ll make the color table, and the detail, which will tell us the color name, and show the color.
Head over to the AppDelegate in this project. Split View controllers should be your root view, and you have two view controllers be the roots for the detail and master view. I usually embed the root view controllers in navigation controllers, so I can add view controllers off that. I did that for the master already. I only need the detail, which I can do like this:
let colorDetailViewController = ColorDetailViewController() let detailNavigationViewController = UINavigationController(rootViewController: colorDetailViewController)
I’ll instantiate a split view controller.
let splitViewController = UISplitViewController()
You assign the master and detail to the viewControllers
property, which is an array of two elements. The first element of the array is the master and the second the detail.
splitVC.viewControllers = [navigationVC,colorDetailVC]
I like doing one more thing to control the width of the master view. I’ll set the property preferredPrimaryColumnWIdthFraction
to a proportion. I’ll set this one to 1/3.
splitVC.preferredPrimaryColumnWidthFraction = 1/3
Finally, assign the splitViewController
to the window
‘s rootViewController
.
window?.rootViewController = splitViewController
Build and run this on an iPhone XR simulator. You’ll just get a single view of the detail. On a compressed width, such as you get in portrait, you get only one of the two views.
On a regular width you’ll get both. Rotate the simulator with a command-left arrow. In Portrait, you see the master and detail.
If you tap a color, it still doesn’t do anything in the detail. I’d like to show the color on the detail when selected from the mater. These are two view controllers without any way to communicate with each other. I’ll use delegation here.
Stop the app. If you go to HueColorTableViewController
, you’ll see I made a protocol and delegate. I’ll add the delegate’s method to the code here in did select
delegate.didSelectColor(color: colors[row])
In the ColorDetailViewController
, I’ll adopt the delegate
UIViewController,HueColorTableViewControllerDelegate {
I added a callback to change the background color and display the name.
Go back to AppDelegate
and add a line to direct the delegate correctly.
colorTVC.delegate = colorDetailVC
Run that, and you get the same split view controller, but select a color and it appears.
That’s the basics of adding a split view controller to a project programmatically.
Below you’ll find the code for this project. You can also download the code from GitHub here.
// // AppDelegate.swift // ColorPicker // // // An exercise file for iOS Development Tips Weekly // by Steven Lipton (C)2018, All rights reserved // For videos go to http://bit.ly/TipsLinkedInLearning // For code go to http://bit.ly/AppPieGithub // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) // Instantiate root view controllers let hueColorTableViewController = HueColorTableViewController() let colorDetailViewController = ColorDetailViewController() hueColorTableViewController.delegate = colorDetailViewController // Embed in navigation controllers let masterNavigationViewController = UINavigationController(rootViewController: hueColorTableViewController) let detailNavigationController = UINavigationController(rootViewController: colorDetailViewController) // Embed in Split View controller let splitViewController = UISplitViewController() splitViewController.viewControllers = [masterNavigationViewController,detailNavigationController] splitViewController.preferredPrimaryColumnWidthFraction = 1/3 // Root view controller of window window?.rootViewController = splitViewController window?.makeKeyAndVisible() return true } }
// // HueColorTableViewController.swift // ColorPicker // // // An exercise file for iOS Development Tips Weekly // by Steven Lipton (C)2018, All rights reserved // For videos go to http://bit.ly/TipsLinkedInLearning // For code go to http://bit.ly/AppPieGithub // import UIKit protocol HueColorTableViewControllerDelegate{ func didSelectColor(color:ColorEntry) } class HueColorTableViewController:UITableViewController{ var delegate:HueColorTableViewControllerDelegate! = nil var colors:[ColorEntry] = ColorModel().hues(count: 12) override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return colors.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { var cell = tableView.dequeueReusableCell(withIdentifier: "cell") if cell == nil { cell = UITableViewCell(style: .default, reuseIdentifier: "cell") } let row = indexPath.row cell?.contentView.backgroundColor = colors[row].color cell?.textLabel?.text = colors[row].name cell?.textLabel?.numberOfLines = 0 return cell! } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let row = indexPath.row title = colors[row].name delegate.didSelectColor(color: colors[row]) } override func loadView() { super.loadView() } }
// // ColorDetailViewController.swift // ColorPicker // // An exercise file for iOS Development Tips Weekly // by Steven Lipton (C)2018, All rights reserved // For videos go to http://bit.ly/TipsLinkedInLearning // For code go to http://bit.ly/AppPieGithub // import UIKit class ColorDetailViewController: UIViewController, HueColorTableViewControllerDelegate { let colorLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() addLayout() // Do any additional setup after loading the view. } //MARK:- Delegates func didSelectColor(color: ColorEntry) { view.backgroundColor = color.color colorLabel.text = color.name } //MARK:- Layout // All layout methods go here. func addLayout(){ colorLabel.text = "Color Detail" let font = UIFont(name: "AmericanTypewriter", size: 30)! colorLabel.font = font colorLabel.backgroundColor = .lightGray colorLabel.textAlignment = .center colorLabel.numberOfLines = 0 view.addSubview(colorLabel) colorLabel.translatesAutoresizingMaskIntoConstraints = false var constraints = [NSLayoutConstraint]() constraints += [NSLayoutConstraint(item: colorLabel, attribute: .leading, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .leading, multiplier: 1.0, constant: 0)] constraints += [NSLayoutConstraint(item: colorLabel, attribute: .trailing, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .trailing, multiplier: 1.0, constant: 0)] constraints += [NSLayoutConstraint(item: colorLabel, attribute: .top, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .top, multiplier: 1.0, constant: 0)] constraints += [NSLayoutConstraint(item: colorLabel, attribute: .height, relatedBy: .equal, toItem: view.safeAreaLayoutGuide, attribute: .height, multiplier: 1 / 9, constant: 0)] view.addConstraints(constraints) } }
// // ColorModel.swift // ColorPicker // // // An exercise file for iOS Development Tips Weekly // by Steven Lipton (C)2018, All rights reserved // For videos go to http://bit.ly/TipsLinkedInLearning // For code go to http://bit.ly/AppPieGithub // import UIKit class ColorEntry{ var name:String = "" var color:UIColor var hue:CGFloat = 0.0 var brightness:CGFloat = 0.5 var saturation:CGFloat = 1.0 init(name:String,color:UIColor){ self.color = color self.name = name } } class ColorModel{ var colors = [ColorEntry]() init(){ colors = [] } func hues(count:Int)->[ColorEntry]{ colors = [] if count <= 0 {return colors} for hue in 0...count{ let hueValue = CGFloat(hue)/CGFloat(count) let color = UIColor(hue: hueValue, saturation: 1.0, brightness: 1.0, alpha: 1.0) let name = String(format:"H:%04.3f S:1.0 B:1.0 ",hueValue) let colorEntry = ColorEntry(name: name, color: color) colors += [colorEntry] } return colors } func lightnessScale(hue:UIColor,count:Int){ } }