How To Make URLSession Wait For Connectivity

If you use URLSession to start a task with iOS 10 and the network is not available the connection fails at once with an error. There was a small change in iOS 11 that means you can now tell your URLSession to wait until network connectivity is available before trying the connection.

Configuring The Session

To configure a session to wait for connectivity set the waitsForConnectivity boolean to true when creating the session configuration. For example, starting with a default session configuration:

let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForResource = 300
if #available(iOS 11, *) {
  configuration.waitsForConnectivity = true
}
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)

Notes:

  • The default resource timeout interval is 7 days which might be longer than you want to wait. I changed it to five minutes here.
  • As with all session configuration settings you must set the waitsForConnectivity flag before creating the session. The session takes a copy of the configuration so changing it afterwards has no effect on the created session.
  • This API is new in iOS 11 so wrap waitsForConnectivity in an #available if supporting iOS 10 or earlier.
  • The default for waitsForConnectivity is false but note that background sessions always wait for connectivity.
  • Set the delegate when creating the session if you want to know when the session waits for connectivity.

Also remember that URLSession keeps a strong reference to its delegate. If the session owner is also the delegate you can end up with a retain cycle unless you call invalidateAndCancel on the session at some point to release the delegate and break the cycle.

session.invalidateAndCancel() // Don't use session after this

Waiting For Connectivity

Using iOS 10 or earlier if you start a data task and there is no connectivity it fails at once. You get back an error either via the completion handler or the delegate method urlSession(_:task:didCompleteWithError:) depending on which you are using.

In iOS 11 if you set waitsForConnectivity to true the session waits (it will eventually timeout) and only starts the task if the network connection becomes available. You also get a callback to the URLSessionTaskDelegate method urlSession(_:taskIsWaitingForConnectivity:) so you can take some action:

func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
    // waiting for connectivity, update UI, etc.
}

Notes:

  • The session only waits when trying the initial connection. If the network drops after making the connection you get back an error via the completion handler or session delegate as with iOS 10.
  • How long the session waits for connectivity depends on the session resource timeout timeoutIntervalForResource. This defaults to 7 days so you may want to change it to some shorter value when creating the session configuration.
Advertisements
How To Make URLSession Wait For Connectivity

All about Properties in swift

First , let me talk about a property . Suppose if we need to change or access an iVar in your class using an object of your class, then there should be getter and setter methods assigned to the iVar. A property is used mainly when other objects need to change or access the ivars in your object, without manually defining getters and setters, or using @property (in objective – c).

In objective-c, @property declares a property in your class header. Here is an example:

@property (nonatomic, retain) NSString *myString;

@synthesize creates your setter and getter for your property (accessor methods). Without synthesize you have to write your own setter and getter implemention, like getMyString or setMyString (capitalize the first character of your property).

So, the above property declaration is equivalent to:

- (NSString*)myString {}
- (void)setMyString:(NSString*)newValue {}

Properties can be further classified into Stored properties and Computed properties.

Stored Property vs Computed property

Stored properties store constant and variable values as part of an instance, whereas computed properties calculate (rather than store) a value.

►Computed properties are provided by classes, structures, and enumerations.

►Stored properties are provided only by classes and structures.

In its simplest form, a stored property is a constant or variable that is stored as part of an instance of a particular class or structure. Stored properties can be either variable stored properties (introduced by the varkeyword) or constant stored properties (introduced by the let keyword).

The example below defines a structure called FixedLengthRange, which describes a range of integers whose range length cannot be changed once it is created:

struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// the range represents integer values 0, 1, and 2
rangeOfThreeItems.firstValue = 6
// the range now represents integer values 6, 7, and 8

Instances of FixedLengthRange have a variable stored property called firstValue and a constant stored property called length. In the example above, length is initialized when the new range is created and cannot be changed thereafter, because it is a constant property.


Stored Properties of Constant Structure Instances

If you create an instance of a structure and assign that instance to a constant, you cannot modify the instance’s properties, even if they were declared as variable properties:

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// this range represents integer values 0, 1, 2, and 3
rangeOfFourItems.firstValue = 6
// this will report an error, even though firstValue is a variable property

Because rangeOfFourItems is declared as a constant (with the let keyword), it is not possible to change its firstValue property, even though firstValueis a variable property.

NOTE: This behavior is due to structures being value types. When an instance of a value type is marked as a constant, so are all of its properties.

The same is not true for classes, which are reference types. If you assign an instance of a reference type to a constant, you can still change that instance’s variable properties.


Lazy Stored Properties

A lazy stored property is a property whose initial value is not calculated until the first time it is used. You indicate a lazy stored property by writing the lazy modifier before its declaration.

Please refer my article on lazy var : lazy var in ios swift

Computed Properties

In addition to stored properties, classes, structures, and enumerations can define computed properties, which do not actually store a value. Instead, they provide a getter and an optional setter to retrieve and set other properties and values indirectly.

If you define get { } inside the property declaration, it makes that property to a computed property. And it cannot have initial value as when you access the property, it will always call get{} function declared in property.

We can skip the associated get and set keyword and their curly brackets if we do not have to set a value to the variable. Computed property with a getter but no setter is known as a read-only computed property. Check the below example:

var a:String{
return “a”
}
print(a) // prints a

The above declaration is same as:

var a:String{
get {
return “a”
}
}

►We can use an optional setter to the computed variable which accepts a newValue as parameter.

class Alphabets {
var _a:String?
var a:String {
get {
return _a ?? “not set”
}
set(newVal) { //you can use any name for the passed parameter.Default is newValue
_a = newVal
}
}
}
/*----------------------------------------------------*/
let alphabet1 = Alphabets()
alphabet1.a = “a”
print(alphabet1.a) // prints a

If a computed property’s setter does not define a name for the new value to be set, a default name of newValue is used. Here’s an alternative version of the Alphabets class, which takes advantage of this shorthand notation:

class Alphabets {
var _a:String?
var a:String {
get {
return _a ?? “not set”
}
set { // no values passed..
_a = newValue // default value name is newValue
}
}
}
/* — — — — — — — — — — — — — — — — — */
let alphabet1 = Alphabets()
alphabet1.a = “a”
print(alphabet1.a) // prints a

Well, what happens if you try to set a computed property in its own setter?

You cannot do that. It will call the same setter method again and again and it will create an endless loop. The app may crash due to a memory overrun.

By definition, a computed property is one whose value you cannot set because, well, it’s computed. It has no independent existence. The purpose of the setter in a computed property is not to set the value of the property but to set the values of other properties, from which the computed property is computed.

Property Observers:

Property observers observe and respond to changes in a property’s value. Property observers are called every time a property’s value is set, even if the new value is the same as the property’s current value. You can add property observers to any stored properties you define, except for lazy stored properties. We can add property observers to ‘inherited’ property by method ‘overriding’.

You have the option to define either or both of these observers on a property:

  • willSet is called just before the value is stored.
  • didSet is called immediately after the new value is stored.

If you implement a willSet observer, it’s passed the new property value as a constant parameter. You can specify a name for this parameter as part of your willSet implementation. If you don’t write the parameter name and parentheses within your implementation, the parameter is made available with a default parameter name of newValue.

Similarly, if you implement a didSet observer, it’s passed a constant parameter containing the old property value. You can name the parameter or use the default parameter name of oldValue.

Here is an example given by apple to explain the flow:

The willSet and didSet observers for totalSteps are called whenever the property is assigned a new value. This is true even if the new value is the same as the current value.

Note:Computed property should be a variable.

let b:Int {
return 4 //error. error: ‘let’ declarations cannot be computed properties
}

Type Properties :

Instance properties are properties that belong to an instance of a particular type. Every time you create a new instance of that type, it has its own set of property values, separate from any other instance.

You can also define properties that belong to the type itself, not to any one instance of that type. There will only ever be one copy of these properties, no matter how many instances of that type you create. These kinds of properties are called type properties.

In Swift, type properties are written as part of the type’s definition, within the type’s outer curly braces, and each type property is explicitly scoped to the type it supports.

You define type properties with the static keyword. For computed type properties for class types, you can use the class keyword instead to allow subclasses to override the superclass’s implementation.

static and class both associate a method with a class, rather than an instance of a class. The difference is that subclasses can override class methods; they cannot override static methods.

The example below shows the syntax for type properties:

In the above example, we tried to override a static func in the subclass Enemybut got an error like : error: cannot override static method .

If you enjoyed reading this post and found it useful, please share and recommend it so others can find it 💚💚💚💚💚💚 !!!!

Thanks!!

All about Properties in swift

SiriKit Tutorial for iOS

SiriKit works using a set of domains, which represent related areas of functionality, such as Messaging.

Within each domain is a set of intents, which represent the specific tasks that the user can achieve using Siri. For example, within the Messaging domain, there are intents for sending a message, searching for messages and setting attributes on a message.

Each intent is represented by an INIntent subclass, and has associated with it a handler protocol and a specific INIntentResponse subclass for you to talk back to SiriKit.

Language processing in your app boils down to SiriKit deciding which intent and app the user is asking for, and your code checking that what the user is asking makes sense or can be done, and then doing it.

Note: For a full list of the available domains and intents, check out the Intents Domains section in the SiriKit programming guide at: apple.co/2d2yUb8

Would You Like to Ride in my Beautiful Balloon?

First, download the starter sample project here. The sample project for this SiriKit tutorial is WenderLoon, a ride-booking app like no other. The members of the Razeware team are floating above London in hot air balloons, waiting to (eventually) pick up passengers and take them to… well, wherever the wind is blowing. It’s not the most practical way to get around, but the journey is very relaxing. Unless Mic is driving. :]

Open up the sample project. Before you can start, you’ll need to amend the bundle identifier of the project so that Xcode can sort out your provisioning profiles. Using Siri needs entitlements, and you need to run it on a device, which means you need your own bundle ID.

Select the WenderLoon project in the project navigator, then select the WenderLoon target. Change the Bundle identifier from com.razeware.WenderLoon to something unique; I’d suggest replacing razewarewith something random.

In the Signing section choose a development team.

Select the WenderLoonCore framework target and change the bundle identifier and select a development team there as well.

Connect a device running iOS 10 and build and run to confirm that everything is working:

You’ll see some balloons drifting somewhere over London. The app doesn’t do very much else — in fact, you’ll be doing the rest of your work in an extension.

Add a new target using the plus button at the bottom of the target list, or by choosing File\New\Target….

Choose the iOS/Application Extension/Intents Extension template.

IntentsExtensionTemplate

On the next screen, enter RideRequestExtension for the product name. Don’t check the Include UI Extension box. If you’re prompted to activate a new scheme, say yes.

A new target and group have been added to your project. Find IntentHandler.swift in the RideRequestExtension group and replace the entire contents of the file with this:

import Intents

class IntentHandler: INExtension {
    
}

Like a lot of Apple template code, there’s a blizzard of nonsense in there that stops you from really understanding each piece. INExtension is the entry point for an Intents extension. It only has one job, which is to provide a handler object for the intent or intents that your app supports.

As mentioned earlier, each intent has an associated handler protocol which defines the methods needed for dealing with that particular intent.

Select the RideRequestExtension scheme then add a new file using File\NewFile…. Choose the Swift File template, name the file RideRequestHandler.swift and make sure it is in the RideRequestExtension group and RideRequestExtension target.

Add the following code to the new file:

import Intents

class RideRequestHandler: 
  NSObject, INRequestRideIntentHandling {
  
}

INRequestRideIntentHandling is the protocol for handling the — you’ve guessed it — ride request intent. It only has one required method.

Add the following code:

func handle(requestRide intent: INRequestRideIntent,
            completion: @escaping (INRequestRideIntentResponse) -> Void) {
  let response = INRequestRideIntentResponse(
    code: .failureRequiringAppLaunchNoServiceInArea,
    userActivity: .none)
  completion(response)
}

This method fires when the user gets to the point where they are ready to book the ride. That’s a little ahead of where the rest of your code is, so at the moment it just returns a response with a failure code.

Switch back to IntentHandler.swift and add the following method:

override func handler(for intent: INIntent) -> Any? {
  if intent is INRequestRideIntent {
    return RideRequestHandler()
  }
  return .none
}

Here, you’re returning your new request handler object if the intent is of the correct type. The only type of intent you’ll be dealing with is the INRequestRideIntent. This has to be declared in another place as well, so that Siri knows it can direct requests to your app.

Open Info.plist inside the RideRequestExtension group and find the NSExtension dictionary. Inside there is an NSExtensionAttributes dictionary which contains an IntentsSupported array. The template is for a messages extension, which means the array contains some messaging intents which you don’t support.

Delete those intents and add in an INRequestRideIntent line:

EditingPlist

There are a few more hoops to jump through before you can use Siri. First, you need to ask the user’s permission. Open AppDelegate.swift in the main WenderLoon group, and you’ll see a stub method called requestAuthorisation().

At the top of the file, import the Intents framework:

import Intents

Then replace the //TODO comment with this code:

INPreferences.requestSiriAuthorization { status in
  if status == .authorized {
    print("Hey, Siri!")
  } else {
    print("Nay, Siri!")
  }
}

Permission requests now come with usage strings which are displayed to the user when the dialog displays. Open Info.plist from the WenderLoon group and find the Privacy – Location… entry.

Add a new entry there, for Privacy – Siri Usage Description (it should autocomplete) and enter a usage string:

Finally, you need to add the Siri entitlement to the app. Select the project, then the WenderLoon target, then the Capabilities tab. Switch on Siri:

Here’s a summary of the steps required to add Siri to your app:

  • Add an Intents extension
  • Create appropriate handler objects
  • Return the handler objects in your INExtension subclass
  • Declare the supported intents in the Info.plist of the extension
  • Request the user’s permission to use Siri
  • Add a Siri usage description to the app’s Info.plist
  • Add the Siri entitlement to the app

After all that, select the WenderLoon scheme (not the extension) and build and run. You’ll get asked to enable Siri:

After all that effort, you really want to make sure you tap OK. If all works well, you should see “Hey, Siri!” printed in the console.

Now the real fun begins. Back in Xcode, change to the RideRequestExtension scheme. Build and run, and choose Siri from the list of applications. Siri will start on your device and you can start having the first of many fun conversations.

Try saying “Book a ride using WenderLoon from Heathrow airport”, and if Siri can understand you, you should see something like the following:

SiriRoundOne

That’s the basic setup complete. Remember, at the moment you’re always returning a response saying that there’s no service in the area, which is what you can see above. In the next sections you’ll work through the detail of handling an intent properly.

99 (passengers in) Red Balloons

Handling an intent is a three-stage process. The first stage is called Resolution. In this stage, your extension has to confirm that all of the information it needs about the intent is present. If there is information missing, Siri can ask the user additional questions.

The information varies depending on the particular intent. For the ride request intent, there are the following parameters:

  • Pickup location
  • Drop-off location
  • Party size
  • Ride option
  • Payment method
Note: If your app isn’t interested in some of the parameters, such as if you only accept Apple Pay for payments, then you can ignore them.

Each parameter comes with a related method in the handler protocol. Remember that you’re using the INRequestRideIntentHandling for handling intents in this app. That protocol has methods for resolving each of the parameters above. Each one receives a ride request intent as a parameter and has a completion block, which you call when you’ve processed the intent. The completion block takes an INIntentResolutionResult subclass as a parameter.

The resolution result tells Siri what to do next, or if everything is OK, it moves on to the next parameter.

That all sounds a little abstract, so here’s a diagram:

Resolution

 

Open RideRequestHandler.swift and add the following method:

func resolvePickupLocation(forRequestRide intent: INRequestRideIntent, with completion: @escaping (INPlacemarkResolutionResult) -> Void) {
  if let pickup = intent.pickupLocation {
    completion(.success(with: pickup))
  } else {
    completion(.needsValue())
  }
}

This method resolves the pickup location. The completion block takes a INPlacemarkResolutionResultparameter, which is the specific subclass for dealing with location values in the Intents framework. Here you accept any pickup location that arrives with the intent. If there is no pickup location, you tell Siri that a value is required.

Build and run the app, and ask Siri to book you a ride using WenderLoon, giving no extra information.

AskForPickup

You supplied no pickup information in the original intent, so the resolution method tells Siri to ask for more data. If you then say a location, the resolution method is called again. The resolution method will get called multiple times until you end up with a success or a failure.

However, the handler object is initialized from scratch for each separate interaction with Siri. A different instance of RideRequestHandler deals with each interaction, which means you cannot use any state information on the handler when dealing with intents.

Back in Xcode, add another resolution method, this time for the drop-off location:

func resolveDropOffLocation(forRequestRide intent: INRequestRideIntent, with completion: @escaping (INPlacemarkResolutionResult) -> Void) {
  if let dropOff = intent.dropOffLocation {
    completion(.success(with: dropOff))
  } else {
    completion(.notRequired())
  }
}

Here you’re allowing a ride with no drop-off location to go ahead. This is actually quite sensible, considering you have absolutely no control over where a hot air balloon will take you. If you build and run, Siri will use a drop-off location that you supply, but it won’t try and fill in the gaps if there isn’t one present.

As well as simply accepting any value that’s passed in as an intent parameter, you can also perform a bit of business logic in there. In many cases, this will involve the same logic used in the main app. Apple recommends that you put code such as this in a separate framework that can be shared between your extension and the main app.

That’s why the sample project contains the WenderLoonCore framework. Bring that framework into the extension by adding the following statement to the top of RideRequestHandler.swift:

import WenderLoonCore

Then add the following property and initializer to RideRequestHandler:

let simulator: WenderLoonSimulator

init(simulator: WenderLoonSimulator) {
  self.simulator = simulator
  super.init()
}

WenderLoonSimulator is an object which contains the business logic for the app. Open IntentHandler.swift and add the following to the top of the file:

import WenderLoonCore

let simulator = WenderLoonSimulator(renderer: nil)

Then replace the line where the request handler is created (it will have an error on it) with the following:

return RideRequestHandler(simulator: simulator)

Now your request handler will be able to access the business logic from the rest of the app.

Back in RideRequestHandler.swift, add the following method for resolving the number of passengers:

func resolvePartySize(forRequestRide intent: INRequestRideIntent, with completion: @escaping (INIntegerResolutionResult) -> Void) {
  switch intent.partySize {
  case .none:
    completion(.needsValue())
  case let .some(p) where simulator.checkNumberOfPassengers(p):
    completion(.success(with: p))
  default:
    completion(.unsupported())
  }
}

This will ask for a number of passengers if the intent doesn’t already contain that information. If the number of passengers is known, it is validated against the rules held in the WenderLoonSimulator object. The maximum number of passengers is four. Build and run and see what happens with different party sizes:

TooManyPassengers

You’ve seen that the resolution stage works by dealing with a single parameter at a time. In the next stage, you can handle the final intent with all of the parameters resolved.

The Confirmation stage of intent handling happens after all of the parameters have been resolved. As with resolution, there are delegate methods specific to each intent. The delegate method has a similar signature to the resolution methods, but there is only one per intent.

Add the following to RideRequestHandler.swift:

func confirm(requestRide intent: INRequestRideIntent, completion: @escaping (INRequestRideIntentResponse) -> Void) {
  let responseCode: INRequestRideIntentResponseCode
  if let location = intent.pickupLocation?.location,
    simulator.pickupWithinRange(location) {
    responseCode = .ready
  } else {
    responseCode = .failureRequiringAppLaunchNoServiceInArea
  }
  let response = INRequestRideIntentResponse(code: responseCode, userActivity: nil)
  completion(response)
}

Here you use a method from the simulator to check that the pickup location is in range. If not, you fail with the “no service in area“ response code.

Sure, you could have performed this check when resolving the pickup location. But then you wouldn’t have seen any implementation at all! :] You can also use this method to ensure that you had connectivity to your services, so the booking could go ahead. This method is called just before the confirmation dialog is shown to the user.

Try to book a ride with a pickup location more than 50 km away from London, and you’ll receive an error telling you there is no service in the area.

Note: If you don’t live near London, edit WenderLoonCore > WenderLoonSimulator.swift > pickupWithinRange(_:) and add a few more zeros to the radius.

You’ve dealt with the first two phases of a Siri interaction: resolution and confirmation. The final phase is where you actually take that intent and convert it into something actionable.

You Can’t Handle the Truth

You implemented a handler way back in the first section of the SiriKit tutorial. All it did was return a failure code, saying there was no service in the area. Now, you’re armed with a fully populated intent so you can perform more useful work.

After the user has seen the confirmation dialog and has requested the ride, Siri shows another dialog with the details of the ride that has been booked. The details of this dialog will differ between the different intents, but in each case you must supply certain relevant details. Each intent actually has its own data model subset, so you need to translate the relevant part of your app’s data model to the standardized models used by the Intents framework.

Switch schemes to the WenderLoonCore framework, add a new Swift file to the Extensions group and name it IntentsModels.swift. Replace the contents with the following:

import Intents

// 1
public extension UIImage {
  public var inImage: INImage {
    return INImage(imageData: UIImagePNGRepresentation(self)!)
  }
}

// 2
public extension Driver {
  public var rideIntentDriver: INRideDriver {
    return INRideDriver(
      personHandle: INPersonHandle(value: name, type: .unknown),
      nameComponents: .none,
      displayName: name,
      image: picture.inImage,
      rating: rating.toString,
      phoneNumber: .none)
  }
}

Here’s what each method does:

  1. The Intents framework, for some reason, uses its own image class INImage. This UIImage extension gives you a handy way to create an INImage.
  2. INRideDriver represents a driver in the Intents framework. Here you pass across the relevant values from the Driver object in use in the rest of the app.

Unfortunately there’s no INBalloon. The Intents framework has a boring old INRideVehicle instead. Add this extension to create one:

public extension Balloon {
  public var rideIntentVehicle: INRideVehicle {
    let vehicle = INRideVehicle()
    vehicle.location = location
    vehicle.manufacturer = "Hot Air Balloon"
    vehicle.registrationPlate = "B4LL 00N"
    vehicle.mapAnnotationImage = image.inImage
    return vehicle
  }
}

This creates a vehicle based on the balloon’s properties.

With that bit of model work in place you can build the framework (press Command-B to do that) then switch back to the ride request extension scheme.

Open RideRequestHandler.swift and replace the implementation of handle(intent:completion:) with the following:

// 1
guard let pickup = intent.pickupLocation?.location else {
  let response = INRequestRideIntentResponse(code: .failure,
    userActivity: .none)
  completion(response)
  return
}

// 2
let dropoff = intent.dropOffLocation?.location ??
  pickup.randomPointWithin(radius: 10_000)

// 3
let response: INRequestRideIntentResponse
// 4
if let balloon = simulator.requestRide(pickup: pickup, dropoff: dropoff) {
  // 5
  let status = INRideStatus()
  status.rideIdentifier = balloon.driver.name
  status.phase = .confirmed
  status.vehicle = balloon.rideIntentVehicle
  status.driver = balloon.driver.rideIntentDriver
  status.estimatedPickupDate = balloon.etaAtNextDestination
  status.pickupLocation = intent.pickupLocation
  status.dropOffLocation = intent.dropOffLocation
  
  response = INRequestRideIntentResponse(code: .success, userActivity: .none)
  response.rideStatus = status
} else {
  response = INRequestRideIntentResponse(code: .failureRequiringAppLaunchNoServiceInArea, userActivity: .none)
}

completion(response)

Here’s the breakdown:

  1. Theoretically, it should be impossible to reach this method without having resolved a pickup location, but hey, Siri…
  2. We’ve decided to embrace the randomness of hot air balloons by not forcing a dropoff location, but the balloon simulator still needs somewhere to drift to.
  3. The INRequestRideIntentResponse object will encapsulate all of the information concerning the ride.
  4. This method checks that a balloon is available and within range, and returns it if so. This means the ride booking can go ahead. If not, you return a failure.
  5. INRideStatus contains information about the ride itself. You populate this object with the Intents versions of the app’s model classes. Then, you attach the ride status to the response object and return it.
Note: The values being used here aren’t what you should use in an actual ride booking app. The identifier should be something like a UUID, you’d need to be more specific about the dropoff location, and you’d need to implement the actual booking for your actual drivers :]

Build and run; book a ride for three passengers, pickup somewhere in London, then confirm the request. You’ll see the final screen:

ConfirmedDefaultScreen

Hmmm. That’s quite lovely, but it isn’t very balloon-ish. In the final part, you’ll create custom UI for this stage!

Making a Balloon Animal, er, UI

To make your own UI for Siri, you need to add another extension to the app. Go to File\New\Target… and choose the Intents UI Extension template from the Application Extension group.

Enter LoonUIExtension for the Product Name and click Finish. Activate the scheme if you are prompted to do so. You’ll see a new group in the project navigator, LoonUIExtension.

A UI extension consists of a view controller, a storyboard and an Info.plist file. Open the Info.plist file and, the same as you did with the Intents extension, change the NSExtension/NSExtensionAttributes/IntentsSupported array to contain INRequestRideIntent.

Each Intents UI extension must only contain one view controller, but that view controller can support multiple intents.

Open MainInterface.storyboard. You’re going to do some quick and dirty Interface Builder work here, since the actual layout isn’t super-important.

Drag in an image view, pin it to the top, left and bottom edges of the container and set width to 0.25x the container width. Set the Content Mode to Aspect Fit.

Drag in a second image view and pin it to the top, right and bottom edges of the container and set the same width constraint and Content Mode.

Drag in a label, pin it to the horizontal and vertical center of the view controller and set the font to System Thin 20.0 and the text to WenderLoon.

Drag in another label, positioned the standard distance underneath the first. Set the text to subtitle. Add a constraint for the vertical spacing to the original label and another to pin it to the horizontal center.

Make the background an attractive blue color.

This is what you’re aiming for:

UIExtensionStoryboard

Open the assistant editor and create the following outlets:

  • The left image view, called balloonImageView
  • The right image view, called driverImageView
  • The subtitle label, called subtitleLabel

In IntentViewController.swift, import the core app framework:

import WenderLoonCore

You configure the view controller in the configure(with: context: completion:) method. Replace the template code with this:

// 1
guard let response = interaction.intentResponse as? INRequestRideIntentResponse
  else {
    driverImageView.image = nil
    balloonImageView.image = nil
    subtitleLabel.text = ""
    completion?(self.desiredSize)
    return
}

// 2
if let driver = response.rideStatus?.driver {
  let name = driver.displayName
  driverImageView.image = WenderLoonSimulator.imageForDriver(name: name)
  balloonImageView.image = WenderLoonSimulator.imageForBallon(driverName: name)
  subtitleLabel.text = "\(name) will arrive soon!"
} else {
// 3
  driverImageView.image = nil
  balloonImageView.image = nil
  subtitleLabel.text = "Preparing..."
}

// 4
completion?(self.desiredSize)

Here’s the breakdown:

  1. You could receive any of the listed intents that your extension handles at this point, so you must check which type you’re actually getting. This extension only handles a single intent.
  2. The extension will be called twice. Once for the confirmation dialog and once for the final handled dialog. When the request has been handled, a driver will have been assigned, so you can create the appropriate UI.
  3. If the booking is at the confirmation stage, you don’t have as much to present.
  4. Finally, you call the completion block that has been passed in. You can vary the size of your view controller and pass in a calculated size. However, the size must be between the maximum and minimum allowed sizes specified by the extensionContext property. desiredSize is a calculated variable added as part of the template that simply gives you the largest allowed size.

Build and run and request a valid ride. Your new UI appears in the Siri interface at the confirmation and handle stages:

CustomUI1

Notice that your new stuff is sandwiched in between all of the existing Siri stuff. There isn’t a huge amount you can do about that. If your view controller implements the INUIHostedViewSiriProviding protocol then you can tell Siri not to display maps (which would turn off the map in the confirm step), messages (which only affects extensions in the Messages domain) or payment transactions.

SiriKit Tutorial for iOS