You’ve probably used ranges in loops, without knowing it, but ranges are really a type in Swift. Have you ever thought about ranges and all their power? I’ll show you a few things you might want to know about Swift ranges. I’ve put a playground into a Project for an exercise file. I added there an array to play with
Here’s the one place I’m sure you’ve seen a range: in a for
loop. Commonly you’ll see them as a closed range using the ...
operator, which include all values in the range. This code
Adds 0+1+2+3+4+5 to get 15
A half-open range takes one less than the number, by changing the last dot for a less than symbol(..<
). This half-open example goes from 0 to 4. Which gives us a total
of 10.
Open and close ranges are their own type Range
, not to be confused with NSRange
. I can assign a range like any other type. Note here the upper bound is outside the range. .
Then use that value as the range in my for
loop.
You can also use a range directly in an array’s subscript. I can make a subarray of elements like this.
which makes a small array of the second, third, and fourth elements of the array.
You can use an half open range here too.
You can stick a count for the array in the range, but there is a faster way. You can use fully open range. Open ranges don’t specify one bound of the range. For example to the end of the array like this:
Or the beginning to iterate from the beginning to an end.
You also can test for membership with the contains property. To see if my range contains 5 I’d use
Since I set range= 0..<5
this returns false as 5 is outside the range. If I look for 2
that returns true because 2 is between 1 and 4
the contains method become handy because you can use it to test if an index is within a range. For a simple example, I’ll write a snippet of code to check if an index is in a given array. I’ll check for element 4 and get Ice cream.
I check for 42, I’ll get nothing
I’ve stuck to Integers here, but ranges can be other types as well. Four example, you can add a range of Double
, then check if a valur exists inthat range quickly.
One important caution I mentioned earlier: Unlike other Swift types, Range
and NSRange
is not the same thing.
NSRange
is a location and a length , Range
is upper and lower bounds. Closed ranges might be a bit easier to convert between the two, but open ranges make it very difficult to convert from one to another. Some API use Range
and some NSRange
. Make sure you know which one you are using.
You can also download this from Github here: If you copy and paste this into a Swift playground in iPad, the copy below will format itself.
import UIKit
//:# Range Demo
//
//: A Demo 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
//:
//: A swift playground formatted for iPad playgrounds
let myDesserts = ["Cannoli","Mochi","Bunelos","Pecan Pie","Ice Cream"]
//:## Basic Ranges
//: Closed range
var total = 0
for number in 0...5{
total += number
}
total
//: Half Open range
total = 0
for number in 0..<5{
total += number
}
total
var doubleRange = 0.1...6.7
doubleRange.contains(Double.pi)
//: Assigning ranges
var range = 0..<5
//:Range
is notNSRange
var nsRange = NSRange(location: 0, length: 4)
total = 0
for number in range{
total += number
}
total
//: Use as array subscripts
var eating = myDesserts[2...4]
eating = myDesserts[2..<4]
eating = myDesserts[2...]
eating = myDesserts[...3]
//: Membership
range.contains(5)
range.contains(2)
let a = 42
range = 0..<myDesserts.count
var eat = ""
if range.contains(a){
eat = myDesserts[a]
} else {
eat = "nothing"
}
eat
One of the really cool features of the App development process is getting real users to beta test your app. iOS has a great way to do this with the test flight app. This week, I’ll summarize the steps to set up Test Flight for your app to beta test. I’ll be using my own app MiFitnessTrails in Beta, so there’s no exercise files this time.
Your first step is to make an App ID. You’ll need to have a developer license for this. Go to your accounts at developer.apple.com/account and select the Certificates, ID’s and profiles button,
Then click App ID’s You see your current App ID’s.
I tend to use a specific App ID for projects I’m producing, and you can make one by pressing the Plus at top.
Register the App Fill in the information requested, using the bundle identifier for your app, your Developer ID, and the services you need. I’m keeping to the default services for now. That will set up a record like this:
Your second step is to set the app up in App Store Connect as you would an app you are publishing.
Go to My Apps and hit the plus button.
Select New App in the drop down that appears
You get the App registration screen.
Once completed and you press Create, Apple will process your request and set up an entry in App Store Connect. You’ll go into that entry. The first screen is for preparing for App Store permissions, and Its a good idea to fill out as much as you can here for final publishing.
You’ll see a tab for TestFlight. Click that. On the sidebar, click Test information. Fill out a description, an email for responses from your testers and a Marketing URL. While marketing documents can be here, if you are still working on user documentation, this is the URL your testers can get them from. Also include a privacy policy URL if you are collecting or sharing information.
App review works differently for Test Flight than for the App store. You are only reviewed once for each test group or version, with your initial build submission. Place your review information here. After your first build is reviewed , you can deploy new builds directly, without approval.
Your next steps are in Xcode. Under Targets, make sure you have all your versioning and build info correct for submission. Builds must be unique each time. I need to change my build number to 14.
Set your simulator to Generic iOS Device. You need to archive the project and you can only do that wiht this setting or connected to a live device. Select Product>Archive and archive the app. Go to the Archives, which might show up for you after archives or Select Window>Organzer(Option-Shift-Command-O). Hit the Archives tab at top.
Select the top archive for build 14. You can validate or distribute the app. I tend to use Validate first, so I can catch any errors I made before submitting. In actual workflows, it saves a lot of time, doing all the steps of a Distribute locally so you can correct your mistakes a lot faster. I know this one works, so I’ll click Distribute.
Leave all the defaults of iOS AppStore and Upload, then I leave all these defaults checked
In the next question. I automatically manage signing. Hit next one last time. Wait as the archinve is processed. I’ll get a summary screen when done. When Processed, I’ll upload it.
This will take some time, as much as ten minutes to an hour depending on the size of your files, so again do some waiting.
While we are waiting, head back to App Store Connect. I’ll set up testers. You’ll see on the side a button New Group.
If you Click that. It show another form for creating the group. I already set up a MakeAppPie list. Since creating a new group would mean a two-day review, I’ll skip creating a new one and show you the created one. There are two tabs, a Testers and a Build tab.
In the Testers tab, you list your testers. All testers need a first name, last name and e-mail address. You can manually add testers, import a file or use the Public Link to get users to join. For example, If you wanted to join this beta test, you can go tohttps://testflight.apple.com/join/99XfjB8r . It would add you to this test group.
You’ll need a build for this, and you can and that in the next tab. Before we do, Let’s check on the build. go back to Xcode and You’ll find the upload was sucessful.
Press Done to dismiss the window. Head back to iTunes connect and in the sidebar select iOS Builds
You’ll see the new build is there. If not, refresh your browser.
In the MiFitness Trails App, you’ll see the build 14 on top of several other builds. Builds have expiration dates which are 90 days from the submission date.
On build 14, there are more questions about encoding. Click the trangle and you’ll get a link.
Then You’ll get a question about changes to encoding. Answer the yes/no question and hit Start Internal Testing. Internal testers are people who have priviledges on your Apple developer account. If you have notifications on for Test flight on your phone, You’ll probably get a notification like I did on my watch:
Back at the builds tab in your list, click the add button
You’ll get a list of the available builds.
Select the build you want. Hit Next. You’ll get another question about testing information. If your app goes through App review before test flight distribution, and there are user sign-ons and passwords in the app, Apple would like access to a test account to make sure everything works correctly.
I don’t have sign-on information so I’ll leave that unchecked. At the bottom, I’ll make sure to inform my users by checking the Automatically notify testers button. Hit next. You’ll get the testing information form.
Add the improvements you’ve made to the app and more instructions to the beta testers for what you are looking for. The hit the Submit for review button.
If this is your first upload, you added a group, or you changed versions, you’ll go through App review and the status of the build will change to Submitted for review. This has taken about one or two days in my experience. Once reviewed and accepted, or if you only changed the build number, this will be ready The build’s status changes to to Testing.
Your beta testers will get a notification of the new build:
When the beta testers go to the test flight App on their device, they can download and install the new version:
That’s the basics of Test Flight. This, of course, can change as Apple changes processes. Without long wait times for approval, with Test Flight you can get an app into a limited number of devices for real-life testing and gain a lot more insight into the effectiveness and improvements needed in your app. Having a large set of beta testers is great for marketing as well. You can start to find core customers for when the app goes live in the App Store.
If you have an iPhone 7 or 8, you might think the home button is a physical button. If you completely shut off the phone you find it is a solid circle. What makes that click is a haptic. You can easily use haptics in your application to make buttons click or warn the user.
I’d suggest downloading the exercise file. Haptics only work on a live phone. Take a look at the starter file. I have a simple button.
Right-click the document outline and you’ll see I made a Touch Up Inside and touch down event action for the button.
Go to the ViewController.swift code.
There are three types of haptics. The impact generator can be used for a collision of objects on the screen or as I use it a touch of a finger to a button for a click. At the top of the code add this:
let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
The style gives one of three options of .light
, .medium
, or .heavy
impact. I’ll use .medium
. Impact generators need a small bit of time to get ready, so when I push down on the button, I’ll add the prepare method.
impactGenerator.prepare() //need enough time for this to get ready
Then I can use the impact occurred method to give the illusion I’m clicking down on something.
impactGenerator.impactOccurred()
impactGenerator.impactOccurred()
When I lift up my finger I don’t feel anything, but to get the click that I’m releasing the button, I can add the impact occurred again.
impactGenerator.impactOccurred()
To run and experience this code you’ll need a real phone. Simulators don’t do haptics, and neither do websites. I suggest trying this yourself since this as a physical sensation or check out the video of this tip. Run this and tap the switch. You get a feel like you are pushing down a button and then releasing it.
Stop the app. At the top of the code, add a notification feedback generator. These give more complicated haptics to notify a user of an event.
let notificationGenerator = UINotificationFeedbackGenerator()
Comment out the impactOccured
in clickMeUp
.
Add in clickMeUp
notificationFeedbackGenerator.notificationOccurred(.success)
You have a few options for the type of notification:error success
and warning
. I’ll use a success
.
Then run the app again. Press down and you’ll feel the impact click. Release and you’ll get a double tap for success.
Try .error,
run again. You get a different pattern.
The third type of haptic is a selection feedback generator. It is so subtle it’s hard to demonstrate. It’s the haptic you get when spinning a picker view.
Haptic can be used for gaming, and for physical feedback, such as alerting users not looking at their phone. Try to use them only when they are necessary, overuse confuses the user. Also, tie them to visual feedback whenever possible. With the proper uses, haptics can add to your applications a true feel.
Here’s the code for this project. You can find a download of this at GitHub here. You can also watch this lesson at LinkedIn Learning
// // ViewController.swift // Haptics demo // // 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 ViewController: UIViewController { let impactGenerator = UIImpactFeedbackGenerator(style: .medium) let notificationGenerator = UINotificationFeedbackGenerator() @IBAction func clickMeDown(_ sender: UIButton) { impactGenerator.prepare() impactGenerator.impactOccurred() } @IBAction func clickMeUp(_ sender: UIButton) { //impactGenerator.impactOccurred() notificationGenerator.notificationOccurred(.error) } override func viewDidLoad() { super.viewDidLoad() } }
Stack views are supposed to help make layout and auto layout easier, but often they will leave you very frustrated. Take a look at this storyboard, which you can download from the exercise files. It’s a mess.
Let’s discuss some solutions for a mess a like this. You’ll see on the error panel I have a lot of errors, much of this is from stackviews being a set of constraints.
But let’s add one more stack view first. Select the Chicken, fish vegetable and beef buttons. Embed them in a stack view. In the attributes, You’ll see the Axis is vertical.
The stack view guesses at attributes and is often wrong. Change to horizontal. Change the Alignment to Fill and Distribution to Fill Equally.
That gives us a different looking stack view.
Now we can get to one of the first important places to check: the spacing. Xcode uses the inherent spacing of the buttons. A large chunk of this mess is this spacing issues, which is causing some conflicting constraints. Open the document outline and select all the stack views.
I’ll change all the stack views Spacing to 8.
That changes a lot:
Select the Stack view details. The alignment here is trailing. I’ll make that fill.
That already looks better, We’ve got some errors still, one set relating to hot and cold. The Temperature stack view Distribution is set to Fill. This leaves some ambiguity among the views, which you can see in teh error panel.
You can do a couple of things here. The first is use Fill Proportionally, and the size of the text dictates the size of the button.
I like using Fill, and use size constraints on the subviews. I’ll make the Hot and Cold buttons equal widths by control – dragging between them on the outline and select Equal Widths.
That give me equal sized buttons
I’d Like more space for my image. I’ll drag up from the soup image in the document outline to the Stack View Image, select Equal Heights
In the size attributes, Change the Multiplier for the constraint to 2/3 so it is 2/3 of the upper stack view
Nothing changes and there are errors here because I’m on Fill Equally distribution for Stack View Image, and need to be on Fill to use sizing. Change that setting and things look better still
We still have errors. Sometimes they come from outside the stack view. Take a look at the Search for Soup button in the size inspector.
It has a proportional height which conflicts with the vertical sizing of the stack view. Delete the size and more errors disappear.
The Add Crunchies!! button is huge. I can change the size of the Add Crunchies!! button and the soup base buttons to be their intrinsic content size, which is the height of the font with some default padding. An easy way to solve this is to grab a empty view, and place it at the bottom of the stack view.
Set its Background color to Clear. The view had no intrinsic size and can stretch and compress much easier the buttons. I still have errors all over the place. This is another distribution error on the root. Change to Fill equally, and it disappears.
This all looks good until you rotate the phone.
That’s why I made two halves in stack views embedded in a stack view. Click the Stack View Root. We can vary the Axis when in landscape on an iPhone. Hit the + next to the Axis. Change this to Any for Width, so we include plus size phones in landscape orientation. Add the variaton
You’ll get an axis variation. Change it to Horizontal
That looks a lot better.
You can keep going like this. The key is to check yourself on other devices and orientations to make sure you don’t get conflicts.
You’ve seen other apps rotate views, but you may have no idea how to do it yourself. Let me show you one way to rotate views. We’ll make a simple knob control you might be able to use in your apps. Download the example file. On the storyboard, I added an UIImageView
of a knob and a label.
In ViewController.swift
, I set this up to use touch methods for user interaction. Check my earlier tip on touch and the apple pencil on how to use these methods.
You do translations like rotations using the layer property of UIView
. layer
is a CALayer
, and that has a property transform
. In the touchesMoved
method inside the braces, assign the transform
property of the layer:
knob.layer.transform =
There’s a couple of ways to do a transform, but I prefer using the 3d ones over others. Add the CATransform3dMakeRotation
method
CATransform3DMakeRotation(angle: CGFloat, x: CGFloat, y: , z: CGFloat)
knob.layer.transform = CATransform3DMakeRotation(angle: CGFloat, x: CGFloat, y: , z: CGFloat)
It has four parameters. The first is the,angle
and I’ll put a variable named angle
there for the moment, and come back to it. Next, I have x
, y
, and z
, which is the axes I’ll rotate through. To avoid a semester of Linear Algebra, I’ll keep this simple. If these values are zero you don’t rotate on that axis. If one, you rotate.
If you use the x-axis, which is a horizontal line on the device you’ll flip the knob. The vertical line is the y-axis, which also flips the knob. To rotate in the same plane you want the line extending toward the user, the z-Axis.
I’ll set the axis to z rotation on and keep x and y off.
CATransform3DMakeRotation(angle, 0, 0, 1)
Now back to angle
. Like most places in iOS, it is in radians. While most like to think in degrees, there’s a good reason for it to be radians: we can describe all points in the rotation as a number between zero and 2 times pi. Since I can multiply the pi in at the last minute, I can think of numbers only in a range of zero to two, which actually makes using the math a lot easier than 360.
Add a line above to set the angle.
let angle = theta * CGFloat.pi
I’ll use theta
as the constant I’ll multiply to pi.
To get a number to use for theta, I’ll use the x position of a touch on the device.
let currentPoint = touch.location(in: view)
To get this to a value between 2 and zero, I’ll divide this by the width of the view, then multiply that by two
let theta = 2.0 * currentPoint.x / view.frame.width
I’ll also print it to the label
label.text = String(format:"%02.3f pi",theta)
I’ll run this on an iPhone 8 simulator, which seems to handle animation better than an iPhone X. Dragging my mouse over the simulator which simulates touch makes the knob move. You’ll see that higher values rotate clockwise, lower ones counterclockwise.
I can also limit the angle by reducing the multiplier from 2 to some smaller number, say 1.5 for a three-quarter turn.
let theta = 1.5 * currentPoint.x / view.frame.width
I can also add a rotation offset. I’ll limit the knob counterclockwise 90 degrees which is subtracting 0.5
let angle = (theta - 0.5) * CGFloat.pi
Build and run. You get a 3/4 turn of the knob limited in different places. Since touch is not completely accurate even on the sides, you might not get a maximum value.
There’s more you can do with this, but this gives you a few ways to explore with rotating views.
In the last tip, I showed you how to use UIPickerControllers
to make a time interval input. What I didn’t show you is how to output that. In this tip I’ll show you how to read and convert data from a picker that has a separate delegate.
If you download the exercise file, you’ll have a copy of the project as I left it last week. If you go to TimeComponents
, you can see the first problem: this is a delegate of a picker. It does not communicate directly with a view controller. I’m going to set a delegate for this that has two parameters: the time in a string and a time interval.
protocol TimeComponentsDelegate{
func didSelect(timeString:String,seconds:TimeInterval)
}
Then assign the delegate to TimeComponents
var delegate:TimeComponentsDelegate! = nil
In pickerView( didSelectRow:)
, I’ll add this to update the values whenever we change a component.
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
delegate.didSelect(timeString: resultString,seconds: timeInterval(resultString))
}
How do I get the String
and the TimeInterval
? It is easier to get the string first and convert it to seconds. I’m going to make a property resultString
.
Every selection, I’ll clear it.
resultString = ""
Then loop through the components
for index in 0..<componentMax.count{
}
Picker views have a method selectedRow(inComponent)
I’ll grab the selected digit in each component as I loop through
let digit = pickerView.selectedRow(inComponent: index)
If the component is a separator, I’ll add the separator to the string. Otherwise, I’ll add the digit.
if componentMax[index] == 0 {
resultString += separator
} else {
resultString += String(format:"%i",digit)
}
That gets me a string. I’ll use another method for converting the string to a timeInterval
. I will separate out the components by the separator, then add the value of the hours minutes and seconds together.
Under the didSelectRow
, add this
func timeInterval(_ timeString:String)-> TimeInterval!{
}
I’ll add an array of multipliers in seconds for hours (3600) minutes(60) and seconds. I’ll also initialize time
to 0.0
let multipliers = [3600.0,60.0,1.0]
var time = 0.0
I’ll split the string into an array using the separator to split it up.
var timeComponents = timeString.components(separatedBy: separator)
I’ll check to make sure the components array is the same length as the multipliers.
if timeComponents.count > multipliers.count {return nil}
.If so, I’ll loop through the components. I’ll use a guard while casting them to TimeInterval
That will also check for a valid value before multiplying by the multiplier. If invalid I’ll return nil
.
for index in 0..<timeComponents.count{
guard let timeComponent = TimeInterval(timeComponents[index]) else {return nil}
time += timeComponent * multipliers[index]
}
I’ll return the TimeInterval
after the loop.
return time
With all that in place. I’ll set up the delegates in the view controllers. I’ll get a message about the required method. Use the Fix there to load the required method. I’ll set it up to display timeString
.
class ViewController: UIViewController,TimeComponentsDelegate {
func didSelect(timeString: String,seconds:TimeInterval ) {
self.title = timeString
}
Don’t forget to point the delegate correctly.
timeComponents.delegate = self
Then I’ll do the same to the SecondViewController
, this time displaying the time interval:
class SecondViewController: UIViewController,TimeComponentsDelegate {
func didSelect(timeString: String, seconds: TimeInterval) {
title = String(format: "%6.0f seconds", seconds)
}
Note I’m living dangerously here, and not worrying about having nil
values. Don’t forget to point the delegate correctly.
timeComponents.delegate = self
Build and run. On the first view controller your time shows up on top.
Click the button, and try 1:01:01 on the second, and you get 3661 seconds.
Using the techniques from this and the last tip, you can make a powerful picker for selecting times.
Here you’ll find the code for this project. You can also download it from GitHub here:
//
// TimeComponenets.swift
// ProtocolPickerDemo
//
//
// 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 TimeComponentsDelegate{
func didSelect(timeString:String,seconds:TimeInterval)
}
class TimeComponents: NSObject,UIPickerViewDataSource, UIPickerViewDelegate {
let componentMax = [9,9,0,5,9,0,5,9] // 0 = separator
var separator = ":"
var delegate:TimeComponentsDelegate! = nil
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return componentMax.count
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return componentMax[component] + 1
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if componentMax[component] == 0{
return separator
}
return String(format:"%i",row)
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
var resultsString = ""
for index in 0..<componentMax.count{
let digit = pickerView.selectedRow(inComponent: index)
if componentMax[index] == 0 {
resultsString += separator
} else {
resultsString += String(format:"%i",digit)
}
}
delegate.didSelect(timeString: resultsString, seconds: timeInterval(resultsString))
}
func timeInterval(_ timeString:String)-> TimeInterval!{
let multipliers = [3600.0,60.0,1.0]
var time = 0.0
var timeComponents = timeString.components(separatedBy: separator)
if timeComponents.count > multipliers.count {return nil}
for index in 0..<timeComponents.count{
guard let timeComponent = TimeInterval(timeComponents[index]) else {return nil}
time += timeComponent * multipliers[index]
}
return time
}
}
//
// ViewController.swift
// ProtocolPickerDemo
//
//
// 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 ViewController: UIViewController, TimeComponentsDelegate {
func didSelect(timeString: String, seconds: TimeInterval) {
self.title = timeString
}
@IBOutlet weak var picker: UIPickerView!
let timeComponents = TimeComponents()
override func viewDidLoad() {
super.viewDidLoad()
picker.delegate = timeComponents
picker.dataSource = timeComponents
timeComponents.delegate = self
}
}
//
// SecondViewController.swift
// ProtocolPickerDemo
//
// 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 SecondViewController: UIViewController, TimeComponentsDelegate{
func didSelect(timeString: String, seconds: TimeInterval) {
self.title = String(format:"%6.0f seconds",seconds)
}
@IBOutlet weak var picker: UIPickerView!
let timeComponents = TimeComponents()
override func viewDidLoad() {
super.viewDidLoad()
picker.delegate = timeComponents
picker.dataSource = timeComponents
timeComponents.delegate = self
}
}
While there is the date components picker, sometimes you want a picker the gives a time interval in seconds. In this two-part tutorial, Let me show you how to set up one, and discuss some good uses of protocols beyondself
If you download the exercise files, you’ll see I set up a project with two pickers and set outlets for them. I also set up two view controllers and another Swift file I named Time components.
If you are familiar withUIPickerViews,
you know that they use a delegate and data source to set the components and return values. Usually, you set the delegate and data source toself
. However, that means you set the component values in each view controller. I want for my project to have hours, minutes and seconds on every picker. Instead of writing the code for that several times, I’ll make a separate object that has the delegates and data sources, then use it in both view controllers.
Go to time components. You’ll see I already added the delegate and data source methods for you to add the code. There are several ways I usually do this depending on the flexibility I need from the picker, but the easiest starts with an array of the maximum value for each picker component.
let componentMax = [5,9,0,5,9,0,5,9]// 0 = separator
I’ll also set a string to be my separator. I’ll make this avar
so I can change it later.
var separator = ":"
I’ll put a zero for anywhere I want a separator in the components. For the numberOfComponents, I’ll return the count for that array
func numberOfComponents(in pickerView: UIPickerView) -&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; Int { return componentMax.count }
Next, I’ll define the number of rows incomponent
To account for 0, that’s one more than the componentMax
element. I’ll return
return componentMax[component] + 1
I’ll use a simpletitleForRow
method to display this. IfcomponentMax
was 0, return the separator string.
if componentMax[component] == 0 { return seperator }
Otherwise, return a string of that row value.
return String(format: "%i", row)
That sets up the display part. Head to theViewController
.
Add a constant timeComponents
let timeComponents = TimeComponents()
InviewDidLoad,
add the delegate and data source to be TimeComponents.
When the picker needs them, It will use this object.
Do the same for theSecondViewController
. Add the object
let timeComponents = TimeComponents()
Then set the delegates and data sources.
picker.dataSource = timeComponents picker.delegate = timeComponents
And do one more thing. Set the separator to a period.
timeComponents.separator = "."
Run this on the simulator. Move the wheels around and you’ll find you can set a time in the range we requested.
Click the button on top and you’ll get the second view controller and can do the same.
You can see the power of using a protocol for more than one view controller. You don’t always wantself
when you have a standard case for your delegates and data sources. Of course, you will want the value out of this example. In next week’s tip, we’ll look at that. You’ll convert Strings to TimeIntervals,
then send back the data to the view controller.
In a previous tip, I drew rectangles and circles in a UIView. This time, let’s add lines and curves using paths to draw some toast.
Download the playground I set up for you. I set up my code to do all the drawing in the drawLines
method. Go to the closure.
I set up a drawing frame size to be the entire frame size of the view. I also added a few points in the array to draw the toast.
let drawPoints:[CGPoint] = [CGPoint(x: 300, y:300),CGPoint(x: 300, y: 500),CGPoint(x: 500, y: 500),CGPoint(x: 500, y: 300)] let imagerenderer = UIGraphicsImageRenderer(size: frame.size)
Before you draw, you should set a few properties. Start with color. For the fill color of our toast, I’ll add brown using the setFill
method of UIColor
.
UIColor.brown.setFill()
Then I’ll add purple for a contrasting stroke color using the setStroke
method of UIColor
.
UIColor.purple.setStroke()
You can also set the shape of the line cap. You do that in the cgContext
of the closure’s context
.
context.cgContext.setLineCap(.round)
I’ll do the same with the line join giving a slightly curved look to the joints at lines.
context.cgContext.setLineJoin(.round)
The foundation of the drawing is a path. You make a path and then draw that path. You start a new path with a beginPath
context.cgContext.beginPath()
For the first point in our path, you move to a point. I’ll move to the first point in the array
context.cgContext.move(to: drawPoints[0])
To add a line you use the addLine
method. This method specifies the point you are going to, using the current point as a start.
context.cgContext.addLine(to: drawPoints[1])
With one line, we can try out our line. Before we do, draw it first using the drawPath
method. drawPath
has a paramater using
, which has several options for how to draw the path. I can fill the path, leave a stroke or both. I’ll start with the stroke.
context.cgContext.drawPath(using: .stroke)
Run this, and You’ll see a line in the live view.
I’ll add another line.
context.cgContext.addLine(to: drawPoints[2])
Once I have two lines I can startusing fills in drawpath
. I’ll change to .fill
context.cgContext.drawPath(using: .fill)
I’ll run again and get a triangle.
The path assumes the endpoint of the path is the beginning point for the fill. This triangle is still open though. To see the stroke better, I’ll make the line a bigger width. You use the setLineWidth
method on cgContext
, which I’ll get from the context in the closure. So add this, just above the beginPath
line.
context.cgContext.setLineWidth(10)
Change the drawPath
to .fillStroke
to draw a line and a stroke both.
context.cgContext.drawPath(using: .fill)
Run again, and you’ll see the fill connects even if the shape is not closed.
I’ll add one more line:
context.cgContext.addLine(to: drawPoints[3])
Run this. We get a three lines around a filled square.
Next, I’ll add a quad curve, one of several curves. This one has a single control point found in the center of the point. I’m going to at first set it to the midpoint of the curve to make this a line.
context.cgContext.addQuadCurve(to: drawPoints[3], control: CGPoint(x: 400, y: 300))
Run this and you get a square.
The control point on a quad curve curves according to the position of the control point. I’ll move the control point up 100 points.
context.cgContext.addQuadCurve(to: drawPoints[0], control: CGPoint(x: 400, y: 200))
Run again.
And you have a piece of toast. I’ve used Playgrounds here to allow for interactive design, but any UIView can use this technique. Explore lines and quad curves for a variety of applications.