Updating from an API Walkthrough

The following is an example using parts of a Lightwell screen to implement a popup populated by dynamic content responding to a url request.

Before we begin, let’s setup an Xcode project to use as a base template. Note that while all of the functionality below can be included into many other setups, for this tutorial we will build a simple use case around a UIViewController. This will let us focus on the SDK functionality.

From Xcode create a new iOS, Single View App. Download this folder of assets. Include the asset catalog, and include and link the HullabaluStoryKit framework.

At this point if we run the application we should see a blank white screen.

Let’s begin by editing the default ViewController.

First we need to initialize a loading context to read the Lightwell data. We’ll add this as a property on the view controller so we can reference the actions from multiple entry points and preserve all animation targeting data.

Building the Foundation

letloadingContext=LWKLoadingContext("promo")

The second is an API call to get the most up to date information and provide a hook to update the contents based on that response.

At this point we’ve updated our single view controller to call out to an API once it appears, and we have hooks to add our Lightwell integration to.

Adding Components to Screen

The LWKLoadingContext gives us a way to access the raw layer and animation data from the design document. It also provides accessors for default generated UIViews and CAAnimations, or a convenient wrapper LWKAction. You can use as much or as little from the document as you want. For our use case, we’re going to use some of the default generated UIViews and LWKActions:

// Only grab the views we care about in the design from Lightwell.// 1) Add background overlay to screenifletbackground=self.context?.view(for:"background"){background.frame=self.view.boundsself.view.addSubview(background)}// 2) Add popup to screenifletpopup=self.context?.view(for:"popup"){self.view.addSubview(popup)}// 3) Show the popup by running the animation actionself.context?.action(for:"show popup")?.run()

We can add this to our API response. Since we are updating the UI, this will need to be performed on the main thread.

importHullabaluStoryKitclassViewController:UIViewController{letloadingContext=LWKLoadingContext("promo")overridefuncviewDidAppear(_animated:Bool){super.viewDidAppear(animated)self.getRemoteData()}}extensionViewController{funcgetRemoteData(){guardleturl=URL(string:"https://s3.amazonaws.com/lightwell/docs/sample/promo-api.json")else{return}varrequest=URLRequest(url:url)request.httpMethod="GET"lettask=URLSession.shared.dataTask(with:request){(data,response,error)in// Jump back to the main thread.DispatchQueue.main.async{// 1) Add background overlay to screenifletbackground=self.context?.view(for:"background"){background.frame=self.view.boundsself.view.addSubview(background)}// 2) Add popup to screenifletpopup=self.context?.view(for:"popup"){self.view.addSubview(popup)}// 3) Show the popup by running the animation actionself.context?.action(for:"show popup")?.run()}}task.resume()}}

If we run the applicaiton at this point, we should see the white screen from before. Then after however long the api call takes, we should see a popup with some static content animate in.

Interacting with the Popup

Now that we have all of the pieces we need, we can start to add some interactions to dismiss the popup or follow through on the example promo. For this use case we can utilize UIKit‘s built in tap gesture recognizers to add a tap outside of the popup to dismiss, and a tap on the popup’s button to follow through:

// 1) Add a gesture to dissmiss the popupself.context?.view(for:"background")?.addGestureRecognizer(UITapGestureRecognizer(target:self,action:#selector(self.dismissPopup)))// 2) Add a gesture to click through on the popupself.context?.view(for:"button")?.addGestureRecognizer(UITapGestureRecognizer(target:self,action:#selector(self.respondToButton)))

Next we need to add the functions for the gesture recognizers to call. These can be as simple or as complex as needed to handle all of the intended behavior. For now they’ll either play a dismissal animation or a follow animation:

// 1) Add hook for dismissing the popup@objcfuncdismissPopup(){// Hide the popup and backgroundcontext?.action(for:"dismiss popup")?.run()}// 2) Add hook for following through with the popup's call to action@objcfuncrespondToButton(){// Play 'success' animationcontext?.action(for:"follow button")?.run()}

Now if we run the application, we will see the popup with new content pulled from the api. The button will look different since we edited its contents without preserving the text style.

Bringing it all Together

At this point we have a popup implemented utilizing views automatically generated from a design document, animations playing without any need for configuration, and content updated dynamically from an API. Pulling it all together and using the convenience setter, our full ViewController.swift file should look something like this:

importUIKitimportHullabaluStoryKitclassViewController:UIViewController{// Initialize the loading context for the promo popupletcontext=LWKLoadingContext(screenName:"promo")overridefuncviewDidAppear(_animated:Bool){super.viewDidAppear(animated)self.getRemoteData()}funcgetRemoteData(){// Setup API callguardleturl=URL(string:"https://s3.amazonaws.com/lightwell/docs/sample/promo-api.json")else{return}varrequest=URLRequest(url:url)request.httpMethod="GET"lettask=URLSession.shared.dataTask(with:request){(data,response,error)inguardletdata=data,error==nilelse{// Failed to get dataifleterror=error{print(error)}return}// Parse dataguardletparsedData=try?JSONSerialization.jsonObject(with:data,options:[]),letusableData=parsedDataas?[String:String]else{// Failed to read datareturn}// Jump to main thread to edit the UIDispatchQueue.main.async{// Add popup views to screenifletbackground=self.context?.view(for:"background"){background.frame=self.view.boundsself.view.addSubview(background)}ifletpopup=self.context?.view(for:"popup"){self.view.addSubview(popup)}// Update contents based on response, using a convenience function to preserve text style(self.context?.view(for:"title")as?UILabel)?.setText(usableData["title"],keepingAttributes:true)(self.context?.view(for:"copy")as?UILabel)?.setText(usableData["copy"],keepingAttributes:true)(self.context?.view(for:"button title")as?UILabel)?.setText(usableData["button"],keepingAttributes:true)// Add inputs to respond to popupself.context?.view(for:"background")?.addGestureRecognizer(UITapGestureRecognizer(target:self,action:#selector(self.dismissPopup)))self.context?.view(for:"button")?.addGestureRecognizer(UITapGestureRecognizer(target:self,action:#selector(self.respondToButton)))// Show the popup by running the animation actionself.context?.action(for:"show popup")?.run()}}task.resume()}@objcfuncdismissPopup(){// Hide the popup and backgroundcontext?.action(for:"dismiss popup")?.run()}@objcfuncrespondToButton(){// Play 'success' animationcontext?.action(for:"follow button")?.run()}}

Adding some final touches

Action Delegate

Presumably we’d want the button to actually trigger some functionality. We can either add that to the respondToButton function we’ve defined or wait until the animation is finished playing using the LWKAction.delegate:

Images

Just as text content can be changed, images can be swapped out too. Layers linked to a rasterized image are converted to a UIImageView. If the api included a url for an updated image we could add the following:

ifletrawImageURL=usableData["image url"],letimageUrl=URL(string:rawImageURL){letimageDownloadTask=URLSession.shared.dataTask(with:imageUrl){(data,response,error)inguardletdata=data,letimage=UIImage(data:data)else{// Failed to get imageifleterror=error{print(error)}else{print("Unable to read data as image.")}return}// Jump to main thread to edit the UIDispatchQueue.main.async{// Update image content(self.context?.view(for:"image")as?UIImageView)?.image=image// Show the popup by running the animation actionself.context?.action(for:"show popup")?.run()}}imageDownloadTask.resume()}

Adding this to the example above:

importUIKitimportHullabaluStoryKitclassViewController:UIViewController{letcontext=LWKLoadingContext(screenName:"promo")overridefuncviewDidAppear(_animated:Bool){super.viewDidAppear(animated)self.getRemoteData()}funcgetRemoteData(){guardleturl=URL(string:"https://s3.amazonaws.com/lightwell/docs/sample/promo-api.json")else{return}varrequest=URLRequest(url:url)request.httpMethod="GET"lettask=URLSession.shared.dataTask(with:request){(data,response,error)inguardletdata=data,error==nilelse{ifleterror=error{print(error)}return}guardletparsedData=try?JSONSerialization.jsonObject(with:data,options:[]),letusableData=parsedDataas?[String:String]else{return}DispatchQueue.main.async{ifletbackground=self.context?.view(for:"background"){background.frame=self.view.boundsself.view.addSubview(background)}ifletpopup=self.context?.view(for:"popup"){self.view.addSubview(popup)}(self.context?.view(for:"title")as?UILabel)?.setText(usableData["title"],keepingAttributes:true)(self.context?.view(for:"copy")as?UILabel)?.setText(usableData["copy"],keepingAttributes:true)(self.context?.view(for:"button title")as?UILabel)?.setText(usableData["button"],keepingAttributes:true)self.context?.view(for:"background")?.addGestureRecognizer(UITapGestureRecognizer(target:self,action:#selector(self.dismissPopup)))self.context?.view(for:"button")?.addGestureRecognizer(UITapGestureRecognizer(target:self,action:#selector(self.respondToButton)))// 1) Check for image url in responseifletrawImageURL=usableData["image url"],letimageUrl=URL(string:rawImageURL){// 2) Download image from urlletimageDownloadTask=URLSession.shared.dataTask(with:imageUrl){(data,response,error)inguardletdata=data,letimage=UIImage(data:data)else{// Failed to get imageifleterror=error{print(error)}else{print("Unable to read data as image.")}return}DispatchQueue.main.async{// 3) Update image content(self.context?.view(for:"image")as?UIImageView)?.image=image// 4.a) Show the popup after successfully updating imageself.context?.action(for:"show popup")?.run()}}imageDownloadTask.resume()}else{// 4.b) Show the popup if there is no image update expectedself.context?.action(for:"show popup")?.run()}}}task.resume()}@objcfuncdismissPopup(){// Hide the popup and backgroundcontext?.action(for:"dismiss popup")?.run()}@objcfuncrespondToButton(){context?.action(for:"follow button")?.delegate=selfcontext?.action(for:"follow button")?.run()}}extensionViewController:LWKActionDelegate{funcactionDidEnd(_action:LWKAction,finished:Bool){print("decided to follow button")}}

Alternate Approach to Optionals

Since the data for the promo screen is packaged with the application, after testing we can use syntax similar to that used with interface builder. With this we can move away from optional chaining, keeping the code a little more succinct:

importUIKitimportHullabaluStoryKitclassViewController:UIViewController{// Initialize the loading context for the promo popupletcontext:LWKLoadingContext!=LWKLoadingContext(screenName:"promo")// Link to key views in the contextlazyvarbackground:UIView!={returnself.context.view(for:"background")}()lazyvarpopup:UIView!={returnself.context.view(for:"popup")}()lazyvarbutton:UIView!={returnself.context.view(for:"button")}()lazyvarpopupTitle:UILabel!={returnself.context.view(for:"title")as?UILabel}()lazyvarpopupCopy:UILabel!={returnself.context.view(for:"copy")as?UILabel}()lazyvarpopupImage:UIImageView!={returnself.context.view(for:"image")as?UIImageView}()lazyvarbuttonTitle:UILabel!={returnself.context.view(for:"button title")as?UILabel}()overridefuncviewDidAppear(_animated:Bool){super.viewDidAppear(animated)self.getRemoteData()}}extensionViewController{funcgetRemoteData(){guardleturl=URL(string:"https://s3.amazonaws.com/lightwell/docs/sample/promo-api.json")else{return}varrequest=URLRequest(url:url)request.httpMethod="GET"lettask=URLSession.shared.dataTask(with:request){(data,response,error)inguardletdata=data,error==nilelse{// Failed to get dataifleterror=error{print(error)}return}// Parse dataguardletparsedData=try?JSONSerialization.jsonObject(with:data,options:[]),letusableData=parsedDataas?[String:String]else{// Failed to read datareturn}// Jump to main thread to edit the UIDispatchQueue.main.async{self.background.frame=self.view.boundsself.view.addSubview(self.background)self.view.addSubview(self.popup)self.buttonTitle.setText(usableData["button"],keepingAttributes:true)self.popupTitle.setText(usableData["title"],keepingAttributes:true)self.popupCopy.setText(usableData["copy"],keepingAttributes:true)// Add inputsself.background.addGestureRecognizer(UITapGestureRecognizer(target:self,action:#selector(self.dismissPopup)))self.button.addGestureRecognizer(UITapGestureRecognizer(target:self,action:#selector(self.respondToButton)))ifletrawImageURL=usableData["image url"],letimageUrl=URL(string:rawImageURL){// Download imageletimageDownloadTask=URLSession.shared.dataTask(with:imageUrl){(data,response,error)inguardletdata=data,letimage=UIImage(data:data)else{// Failed to get imageifleterror=error{print(error)}else{print("Unable to read data as image.")}return}DispatchQueue.main.async{// Update imageself.popupImage.image=image// Show popupself.context.action(for:"show popup")?.run()}}imageDownloadTask.resume()}else{// Show the popup without an updated imageself.context?.action(for:"show popup")?.run()}}}task.resume()}@objcfuncdismissPopup(){context?.action(for:"dismiss popup")?.run()}@objcfuncrespondToButton(){context?.action(for:"follow button")?.delegate=selfcontext?.action(for:"follow button")?.run()}}extensionViewController:LWKActionDelegate{funcactionDidEnd(_action:LWKAction,finished:Bool){print("decided to follow button")}}

The API is currently in beta and everything inside of these docs will be changing to improve the SDK. If there is a change you’d like to see or have any feedback, please email us at dev@lightwell.pro.