Monday 15 February 2016

Storyboards Tutorial in iOS 9: Part 1

Getting Started

Fire up Xcode and create a new project. Use the Single View Application template as the starting point.
XcodeProject
Fill in the template options as follows:
  • Product Name: Ratings
  • Organization Name: fill this in however you like
  • Organization Identifier: the identifier that you use for your apps
  • Language: Swift
  • Devices: iPhone
  • Use Core Data: not checked
  • Include Unit Tests and UI Tests: not checked
After Xcode has created the project, the main Xcode window looks like this:
Main Window
The new project consists of two classes, AppDelegate and ViewController, and the star of this tutorial: the Main.storyboard file.
This is a portrait-only app, so before you continue, uncheck the Landscape Left and Landscape Right options under Deployment Info > Device Orientation seen in the General project settings shown above .
Let’s take a look at that storyboard. Click Main.storyboard in the project navigator to open it in the Interface Builder editor:
Main Storyboard
The official storyboard terminology for a view controller is “scene”, but you can use the terms interchangeably. The scene is what represents a view controller in the storyboard.
Here you see a single view controller containing an empty view. The arrow pointing at the view controller from the left indicates that it is the initial view controller to be displayed for this storyboard.
Designing a layout in the storyboard editor is done by dragging controls from the Object Library (see bottom-right corner) into your view controller. You’ll see how easy that is in just a moment.
Note: You’ll notice that the default scene size is a square. Xcode 7 enables Auto Layout and Size Classes by default for storyboards. Auto Layout and Size Classes allow you to make flexible user interfaces that can easily resize, which is useful for supporting the various sizes of iPhones and iPads. To learn more about size classes, check out our Adaptive Layout video tutorial series.
In this tutorial you will take the optional step of resizing the scenes in your storyboard so that you can more easily visualise what the final screen will look like.
Before you get to exploring, resize the scene to simulate an iPhone 6/6s.
Select View Controller in the Document Outline. If you don’t see a Document Outline, click this button at the bottom left of the storyboard canvas:
Document Outline Icon
In the Attributes Inspector under Simulated Metrics, change Size to iPhone 4.7 inch
Simulated Metrics
The scene in the storyboard will now show as the size of the iPhone 6 or 6s, which are 4.7 inch iPhones.
“Inferred” is the default setting for Simulated Metrics in storyboards. Simulated Metrics are a visual design aid inside the storyboard that shows what your screen will end up looking like. Just remember that they aren’t used during runtime.
To get a feel for how the storyboard editor works, drag some controls from the Object Library in the lower right into the blank view controller:
Drag Controls
As you drag the controls in, they should show up on the Document Outline on the left:
Document Outline
The storyboard shows the contents of all your view controllers. Currently there is only one view controller (or scene) in your storyboard, but over the course of this tutorial you’ll be adding several others.
There is a miniature version of this Document Outline above the scene called the Dock:
The Dock
The Dock shows the top-level objects in the scene. Each scene has at least a View Controller object, a First Responder object, and an Exit item, but it can potentially have other top-level objects as well. The Dock is convenient for making connections to outlets and actions. If you need to connect something to the view controller, you can simply drag to its icon in the Dock.
Note: You probably won’t be using the First Responder very much. This is a proxy object that refers to whatever object has first responder status at any given time. As an example, you can hook up the Touch Up Inside event from a button to First Responder’s cut: selector. If at some point a text field has input focus then you can press that button to make the text field, which is now the first responder, cut its text to the pasteboard.
Run the app and it should look exactly like what you designed in the editor (yours may look different than the screenshot below – this is just for demonstration and will not be used later in the tutorial):
Simulator Testing
The single View Controller you defined was set as the Initial View Controller – but how did the app load it? Take a peek at the application delegate to find the answer. Open up AppDelegate.swift and you’ll see the source starts with this:
import UIKit
 
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
 
  var window: UIWindow?
 
  func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.
    return true
  }
The @UIApplicationMain attribute at the top of the file designates the AppDelegate class as the entry point for the module. It is a requirement for using storyboards that your application delegate inherits from UIResponderand that it has a UIWindow property. All the methods are practically empty. Even application(_:didFinishLaunchingWithOptions:) simply returns true.
The secret is in the Info.plist file. Click on Info.plist in the Project Navigator and you’ll see this:
Info.plist
Storyboard apps use the key UIMainStoryboardFile, also known as “Main storyboard file base name”, to specify the name of the storyboard that must be loaded when the app starts. When this setting is present, UIApplication will load the named storyboard file, automatically instantiate the “Initial View Controller” from that storyboard, and then put that controller’s view into a new UIWindow object.
You can also see this in the Project Settings under the General tab and Deployment Info section:
Project Settings
Now to create the real Ratings app with several view controllers.

Just Add It To My Tab

The Ratings app you’re about to build has a tabbed interface with two screens. With a storyboard it is really easy to create tabs.
You’ll want to start with a clean storyboard, so switch back to Main.storyboard and delete the view controller you worked with earlier. This can be done by clicking on View Controller in the Document Outline and pressing the Delete key.
Drag a Tab Bar Controller from the Object Library into the canvas. You may want to maximize your Xcode window first, because the Tab Bar Controller comes with two view controllers attached and you’ll need some room to maneuver. You can zoom in and out by double-clicking the canvas, or you can set the zoom scale by ctrl-clicking the canvas and selecting the zoom level.
For convenience, again change the Simulated Metrics to show the scene as an iPhone. As you did before, select Tab Bar Controller in the Document Outline, and on the Attributes Inspector, change Size to iPhone 4.7 inch. This will also change the two embedded view controllers to simulate the iPhone 6 or 6s in the storyboard.
Tab Bar Controller
The new Tab Bar Controller comes pre-configured with two additional view controllers – one for each tab. UITabBarController is a so-called container view controller because it contains one or more other view controllers. Two other common containers are the Navigation Controller and the Split View Controller (you’ll use the Navigation Controller later).
The container Relationship is represented by the arrows between the Tab Bar Controller and the view controllers that it contains. An embed Relationship in particular is signified by the icon seen below in the middle of the arrow body.
Container Relationship
Note: If you want to move the Tab Bar Controller and its attached view controllers as a group, zoom out, and then you can ⌘-click or click and drag to select multiple scenes. This makes it possible to move them around together. (Selected scenes have a thin blue outline.)
Drag a label into the first view controller (currently titled “Item 1”), double click it, and give it the text “First Tab”. Also drag a label into the second view controller (“Item 2”) and give it the text “Second Tab”. This allows you to actually see something happen when you switch between the tabs.
Note: You can’t drag stuff into the scenes when the editor is zoomed out. You’ll need to return to the normal zoom level first by double-clicking in the canvas.
Build & Run, and you’ll see something similar to this in the console:
Ratings[18955:1293100] Failed to instantiate the default view controller for UIMainStoryboardFile 'Main' - perhaps the designated entry point is not set?
Fortunately, the error is pretty clear here – you never set an entry point, meaning you didn’t set the Initial View Controller after you deleted the scene used earlier. To fix this, select the Tab Bar Controller and go to the Attributes Inspector. Check the box that says Is Initial View Controller.
Initial View Controller
In the canvas, the arrow that used to point to the deleted view controller now points at the Tab Bar Controller:
Is Initial View Controller
This means that when you run the app, UIApplication will make the Tab Bar Controller the main screen. Run the app and try it out. The app now has a tab bar and you can switch between the two view controllers with the tabs:
App with Tabs
Tip: To change the initial view controller, you can also drag the arrow between view controllers.
Xcode actually comes with a template for building a tabbed app (unsurprisingly called the Tabbed Application template) that you could have used, but it’s good to know how this works so you can also create a Tab Bar Controller by hand if you have to.
Note: If you connect more than five scenes to the Tab Bar Controller, it automatically gets a More… tab when you run the app. Pretty neat!

Adding a Table View Controller

The two scenes that are currently attached to the Tab Bar Controller are both regular UIViewControllerinstances. You are going to replace the scene from the first tab with a UITableViewController instead.
Click on that first view controller in the Document Outline to select it, and then delete it. From the Object Library drag a new Table View Controller into the canvas in the place where that previous scene used to be:
Table View Controller
Now you want to place the Table View Controller inside a navigation controller. With the Table View Controller selected, choose Editor\Embed In\Navigation Controller from Xcode’s menubar. This adds yet another controller to the canvas:
Navigation Controller
You could also have dragged in a Navigation Controller from the Object Library and embedded the tableview, but this Embed In command is a nice time saver for a common action.
Because the Navigation Controller is also a container view controller (just like the Tab Bar Controller), it has a relationship arrow pointing at the Table View Controller. You can also see these relationships in the Document Outline:
Relationship
Notice that embedding the Table View Controller gave it a navigation bar. Interface Builder automatically put it there because this scene will now be displayed inside the Navigation Controller’s frame. It’s not a real UINavigationBar object, but a simulated one. Simulated Metrics will infer the context around the scene and show a navigation bar when it’s inside a Navigation Controller, a tab bar when it’s inside a Tab Bar Controller, and so on.
The new controllers are currently square shaped. When you embed them inside the Tab Bar Controller as you will in a moment, they will change their simulated size to match the parent scenes.
To connect these two new scenes to the Tab Bar Controller, ctrl-drag from the Tab Bar Controller to the Navigation Controller. When you let go, a small popup menu appears. Choose the Relationship Segue – view controllers option:
Embed VC
This creates a new relationship arrow between the two scenes. This is also an embed Relationship as you saw with the other controllers contained by the Tab Bar Controller.
The Tab Bar Controller has two embed relationships, one for each tab. The Navigation Controller itself has an embed Relationship with the Table View Controller.
When you made this new connection, a new tab was added to the Tab Bar Controller, simply named “Item”. For this app, you want this new scene to be the first tab, so drag the tabs around to change their order:
Drag tab items
Run the app and try it out. The first tab now contains a table view inside a navigation controller.
SimulatorFirstTabWithTableView
Before you put some actual functionality into this app, you need to clean up the storyboard a little. You will name the first tab “Players” and the second “Gestures”. You don’t change this on the Tab Bar Controller itself, but in the view controllers that are connected to these tabs.
As soon as you connect a view controller to a Tab Bar Controller, it is given a Tab Bar Item object which you can see in the Document Outline or the bottom of the scene. You use this Tab Bar Item to configure the tab’s title and image seen on the Tab Bar Controller.
Select the Tab Bar Item inside the Navigation Controller, and in the Attributes inspector set its Title to Players:
Tab Bar Players
Rename the Tab Bar Item for the view controller from the second tab to Gestures in the same manner.
A well-designed app should also put some pictures on these tabs. The resources for this tutorial contains a subfolder named Images. Drag that folder into the Assets.xcassets subfolder in the project.
Back in Main.storyboard, in the Attributes inspector for the Players Tab Bar Item, choose the Players.pngimage.
Players Image
You probably guessed it, but give the Gestures item the image Gestures.png.
A view controller that is embedded inside a Navigation Controller has a Navigation Item that is used to configure the navigation bar. Select the Navigation Item for the Table View Controller in the Document Outline and change its title in the Attributes inspector to Players. .
Navigation Item
Notice that the Scene title in the Document Outline now changes to Players
Alternatively, you can double-click the navigation bar and change the title there. Note that you should double-click the simulated navigation bar in the Table View Controller, not the actual Navigation Bar object in the Navigation Controller.
Run the app and marvel at your pretty tab bar, created without writing a single line of code!
App With Tab Bar Images

Prototype Cells

Prototype cells allow you to easily design a custom layout for your table view cells directly from within the storyboard editor.
The Table View Controller comes with a blank prototype cell. Click on that cell to select it and in the Attributes inspector set the Style option to Subtitle. This immediately changes the appearance of the cell to include two labels.
With so much stackable content on a storyboard, it can sometimes be difficult to click on exactly what you want. If you have trouble, there are several options. One is that you can select the item in the Document Outline to the left of the canvas. The second is a handy hotkey: hold control + shift and click on the area you’re interested in. A popup will appear allowing you to select any element directly under your cursor.
If you’ve used table views before and created your own cells by hand, you may recognize this as the UITableViewCellStyle.Subtitle style. With prototype cells you can either pick one of the built-in cell styles as you just did, or create your own custom design (which you’ll do shortly).
Set the Accessory attribute to Disclosure Indicator and in the Identifier field type PlayerCell. All prototype cells should have a reuse identifier so that you can refer to them in code.
Cell Setup
Run the app, and… nothing has changed. That’s not so strange: you still have to make a data source for the table so it will know what rows to display. That’s exactly what you’re going to do next.
Add a new file to the project. Choose the Cocoa Touch Class template under iOS/Source. Name the class PlayersViewController and make it a subclass of UITableViewController. Uncheck Also create XIB file. Choose the Swift language and hit Next followed by Create.
Players View Controller
Go back to the storyboard and select the Table View Controller (make sure you select the actual view controller and not one of the views inside it). In the Identity inspector, set its Class to PlayersViewController. That is the essential step for hooking up a scene from the storyboard with your custom view controller subclass. Don’t forget this or your class won’t be used!
Players VC Class
From now on when you run the app that table view controller from the storyboard is an instance of the PlayersViewController class.
The table view should display a list of players, so now you will create the main data model for the app – an array that contains Player objects. Add a new file to the project using the Swift File template under iOS/Source. Name the file Player.
Replace the code in Player.swift with:
import UIKit
 
struct Player {
  var name: String?
  var game: String?
  var rating: Int
 
  init(name: String?, game: String?, rating: Int) {
    self.name = name
    self.game = game
    self.rating = rating
  }
}
There’s nothing special going on here. Player is simply a container object for these three properties: the name of the player, the game they’re playing, and a rating of 1 to 5 stars.
You’ll next make an array of test Player objects and then assign it to an array in PlayersViewController. Start by creating a new file using the Swift File template named SampleData. Add this to the end of SampleData.swift:
//Set up sample data
 
let playersData = [ 
  Player(name:"Bill Evans", game:"Tic-Tac-Toe", rating: 4),
  Player(name: "Oscar Peterson", game: "Spin the Bottle", rating: 5),
  Player(name: "Dave Brubeck", game: "Texas Hold 'em Poker", rating: 2) ]
Here you’ve defined a constant called playersData and assigned an array of hard coded Player objects to it.
Now add a Player array property just below class PlayersTableViewController: UITableViewController in PlayersViewController.swift to hold the list of players:
var players:[Player] = playersData
You could simply have set up the sample data in PlayersViewController when defining the players variable. But because this data might later be provided from a plist or an SQL file, it’s wise to handle loading the data outside of the view controller.
Now that you have an array full of Player objects, you can continue hooking up the data source in PlayersViewController. Still in PlayersViewController.swift, replace the table view data source methods with the following:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
  return 1
}
 
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  return players.count
}
The real work happens in cellForRowAtIndexPath. Replace this method, which is currently commented out, with:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)
 -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("PlayerCell", forIndexPath: indexPath)
 
  let player = players[indexPath.row] as Player
  cell.textLabel?.text = player.name
  cell.detailTextLabel?.text = player.game
  return cell
}
The method dequeueReusableCellWithIdentifier(_:forIndexPath:) will check to see if there is an existing cell that can be recycled. If not, it will automatically allocate a prototype cell and return it to you. All you need to do is supply the re-use identifier that you set on the prototype cell in the storyboard editor – in this case PlayerCell. Don’t forget to set that identifier, or this little scheme won’t work!
Run the app, and lo and behold, the table view has players in it!
App With Players
It takes just a few lines of code to use these prototype cells. I think that’s just great!
Note: In this app you’re using only one prototype cell but if your table needs to display different kinds of cells then you can simply add additional prototype cells to the storyboard. You can either duplicate the existing cell to make a new one, or increment the value of the Table View’s Prototype Cells attribute. Be sure to give each cell its own re-use identifier, though.

Designing Your Own Prototype Cells

Using a standard cell style is OK for many apps, but for this app you want to add an image on the right-hand side of the cell that shows the player’s rating (one to five stars). Having an image view in that spot is not supported by the standard cell styles, so you’ll have to make a custom design.
Switch back to Main.storyboard, select the prototype cell in the table view, and on the Attributes inspector, set its Style attribute to Custom. The default labels now disappear.
First make the cell a little taller. Either change the Row Height value in the Size inspector (after checking Custom) or drag the handle at the bottom of the cell. Make the cell 60 points high.
Drag two Label objects from the Objects Library into the cell and place them roughly where the standard labels were previously. Just play with the font and colors in the Attributes Inspector and pick something you like. Set the text of the top label to Name and the bottom label to Game.
Select both the Name and Game labels in the Document Outline using Command+click, and choose Editor\Embed In\Stack View.
Note: Stack views are new in iOS 9 and are brilliant for easily laying out collections of views. You can find out more about stack views in our new book iOS 9 by Tutorials.
Drag an Image View into the cell and place it on the right, next to the disclosure indicator. In the Size Inspector, make it 81 points wide and 35 points high. Set its Mode to Center (under View in the Attributes inspector) so that whatever image you put into this view is not stretched.
Command + click the Stack View and Image View in the Document Outline to select both of them. Choose Editor\Embed in\Stack View. Xcode will create a new horizontal stack view containing these two controls.
Stack View
Select this new horizontal stack view, and in the Attributes Inspector, change the Alignment to Centre and the Distribution to Equal Spacing.
Now for some simple auto layout for this control. At the bottom right of the storyboard, click the Pin icon:
Pin Icon
Change the top constraints to Top: 0, Right: 20, Bottom: 0 and Left: 20. Make sure that the four red pointers to the values are highlighted as in the picture. Click Add 4 Constraints at the bottom of the popover window.
Constraints
If your stack view has orange constraints, it is misplaced. To fix this, select the horizontal stack view and choose Editor\Resolve Auto Layout Issues\Update Frames (in the Selected Views section of the menu). The stack view should position itself correctly and the orange constraint errors go away.
To position the image view within the stack view, select the image view in the Document Outline and choose Editor\Resolve Auto Layout Issues\Add Missing Constraints (in the Selected Views section of the menu).
The final design for the prototype cell looks something like this:
Final Cell
Because this is a custom designed cell, you can no longer use UITableViewCell’s textLabel and detailTextLabel properties to put text into the labels. These properties refer to labels that aren’t on this cell anymore; they are only valid for the standard cell types. Instead, you will use tags to find the labels.
Tags are used here for simplicity. Later in this tutorial you’ll create a custom class that inherits from UITableViewCell and contains properties corresponding to the labels on your cell view.
In the Attributes inspector, set the tag value for the Name label to 100Game label to 101, and the Image Viewlabel 102.
Then open PlayersViewController.swift and add a new method called imageForRating at the end of the class as follows:
func imageForRating(rating:Int) -> UIImage? {
  let imageName = "\(rating)Stars"
  return UIImage(named: imageName)
}
Pretty simple – this returns a different star image depending on the rating. Still in PlayersViewController, change tableView(_:cellForRowAtIndexPath:) to the following:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("PlayerCell", forIndexPath: indexPath) //1
 
  let player = players[indexPath.row] as Player //2
 
  if let nameLabel = cell.viewWithTag(100) as? UILabel { //3
    nameLabel.text = player.name
  }
  if let gameLabel = cell.viewWithTag(101) as? UILabel {
    gameLabel.text = player.game
  }
  if let ratingImageView = cell.viewWithTag(102) as? UIImageView {
    ratingImageView.image = self.imageForRating(player.rating)
  }
  return cell
}
Here’s the breakdown of what you’ve done:
  1. dequeueReusableCellWithIdentifier will dequeue an existing cell with the reuse identifier PlayerCell if available or create a new one if not.
  2. You look up the Player object corresponding to the row being populated and assign it to player.
  3. The labels and images are looked up by their tag on the cell and populated with data from the player object.
That should do it. Now run the app again, and it may look something like this:
Wrong Cell Height
Hmm, that doesn’t look quite right – the cells appear to be squished. You did change the height of the prototype cell, but the table view doesn’t take that into consideration. There are two ways to fix it: you can change the table view’s Row Height attribute, or implement the tableView(tableView:heightForRowAtIndexPath:) method. The former is fine in this case because we only have one type of cell and we know the height in advance.
Note: You would use tableView(tableView:heightForRowAtIndexPath:) if you did not know the height of your cells in advance, or if different rows can have different heights.
Back in Main.storyboard, in the Size inspector of the Table View, set Row Height to 60:
RightHeight
If you run the app now, it looks a lot better!
Proper Row Height
By the way, if you changed the height of the cell by dragging its handle rather than typing in the value, then the table view’s Row Height property was automatically changed too. So it may have worked correctly for you the first time around.

Using a Subclass for the Cell

The table view already works pretty well but I’m not a big fan of using tags to access the labels and other subviews of the prototype cell. It would be much more clean if you could connect these labels to outlets and then use the corresponding properties. As it turns out, you can.
Add a new file to the project, with the Cocoa Touch Class template. Name it PlayerCell and make it a subclass of UITableViewCell. Don’t check the option to create a XIB, as you already have the cell in your storyboard.
Add these properties in the PlayerCell class, just below the class definition:
@IBOutlet weak var gameLabel: UILabel!
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var ratingImageView: UIImageView!
All these variables are IBOutlets, which can be connected up to your scene in the storyboard.
Add this property just below the IBOutlets:
var player: Player! {
  didSet {
    gameLabel.text = player.game
    nameLabel.text = player.name
    ratingImageView.image = imageForRating(player.rating)
  }
}
Whenever the player property is set, it will update the IBOutlets with the correct information.
Move the method imageForRating(_:) from PlayersViewController to the PlayerCell class to keep the cell details all in the same class.
Back in Main.storyboard, select the prototype cell PlayerCell and change its class to PlayerCell on the Identity inspector. Now whenever you ask the table view data source for a new cell with dequeueReusableCellWithIdentifier(_:forIndexPath:), it will return a PlayerCell instance instead of a regular UITableViewCell.
Note that you gave this class the same name as the reuse identifier – they’re both called PlayerCell – but that’s only because I like to keep things consistent. The class name and reuse identifier have nothing to do with each other, so you could name them differently if you wanted to.
Now connect the labels and the image view to these outlets. Navigate to the Connections Inspector in the storyboard and then select the Player Cell from either the canvas or Document Outline. Drag from the nameLabel Outlet in the Connections inspector to the Name label object in either the Document Outline, or the canvas. Repeat for gameLabel and ratingImageView.
Name Label
Important: You should hook up the controls to the table view cell, not to the view controller! You see, whenever your data source asks the table view for a new cell with dequeueReusableCellWithIdentifier, the table view doesn’t give you the actual prototype cell but a copy (or one of the previous cells is recycled if possible).
This means there will be more than one instance of PlayerCell at any given time. If you were to connect a label from the cell to an outlet on the view controller, then several copies of the label will try to use the same outlet. That’s just asking for trouble. (On the other hand, connecting the prototype cell to actions on the view controller is perfectly fine. You would do that if you had custom buttons or other UIControls on your cell.)
Now that you’ve hooked up the properties, you can simplify the data source code a bit. In PlayersViewController, change tableView(_:cellForRowAtIndexPath:) to:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)
    -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("PlayerCell", forIndexPath: indexPath)
        as! PlayerCell
 
    let player = players[indexPath.row] as Player
    cell.player = player
    return cell
}
That’s more like it. You now cast the object that you receive from dequeueReusableCellWithIdentifier to a PlayerCell, and then you can simply pass the correct player to the cell. Setting the player variable in PlayerCell will automatically propagate the values into the labels and image view, and the cell will use the IBOutlets that you wired up in the storyboard. Isn’t it great how using prototype cells makes table views a whole lot less messy?
Run the app and try it out. It should still look the same as before, but behind the scenes it’s now using your own table view cell subclass!

2 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Your blog is a shining example of creativity and insightful content. Keep up the excellent work! VERY WELL DONE..!!

    Now, let's burrow some light into Maven Technology – the epitome of a top-tier, the best digital marketing agency. With cutting-edge strategies, innovative campaigns, and a passion for search engine results, Maven Technology stands out as the go-to choice for businesses seeking unparalleled Digital Marketing Solutions.

    Many Thanks.

    ReplyDelete