Multiple Inheritance in Swift or Protocol Oriented Programming

Swift Programming Language Bird


Flexibility of protocols in Swift gives us an opportunity to write structs or classes that apply something like multiple inheritance. This article tries to explain a simple example of using that pattern.

What is the main purpose of protocols in Swift? Of course protocols talk to structs and classes how they should look. If we signed our data type for the desired protocol and the type has no all the properties and methods which our protocol needs, then compiler can’t launch the program. Besides we are able to sign one custom data type for many protcols and this possibility allows us to use some patterns from multiple inheritance. It's really cool and safe!

For example, let's create a simple app that shows the information about nature object that we randomly get from an array. In the storyboard's scene we need to add an ImageView for object's image or animation, three labels for different information and a button for getting a random object, into the main view. 

Xcode Interface Design

In this example we write a code according MVC pattern. We add a swift file and name it as "ObjectData", the model data will be written over this file. First of all, let's create the main protocol for our structs, it should have common data that we need to display without downcasting. The main protocol must have title, type and coordinates properties. 


protocol ObjectProtocol{
    var title: String {get}
    var type: String {get}
    var coordinateX: Int {get set}
    var coordinateY: Int {get set}
}

We type a keyword {get} that means "get only property" when we need to create an immutable constant in our future object, and {get set} keywords that mean "gettable and settable" when we want to create a variable.

Remember about our UIImageView in the storyboard. UIImageView is able to show a static image or animation of images from array. We can make a struct with optional values that gives us two ways for displaying images.


struct AnimalObject: ObjectProtocol{
    let title: String
    let type: String
    var coordinateX: Int
    var coordinateY: Int
    let animationImages: [String]?
    let image: String?
}


But what about animation duration? Perhaps you want to get random image from some array of images. Do we need more optional values like randomImages, staticImage, animationDuration? I have a better idea - we can create protocols that handle these settings.


protocol AnimationProtocol {
    var animationImages: [String] {get}
    var animationDuration: Double {get}
}
protocol StaticImageProtocol {
    var image: String {get}
    var randomImages: [String] {get}
}

Finally for the model data we need to create two simple structs which could be initialise for animals and not animals instances:

struct AnimalObject: ObjectProtocol, AnimationProtocol {
    let title: String
    let type: String
    var coordinateX: Int
    var coordinateY: Int
    var animationImages: [String]
    var animationDuration: Double
}

struct NotAnimalObject: ObjectProtocol, StaticImageProtocol{
    var title: String
    var type: String
    var coordinateX: Int
    var coordinateY: Int
    var image: String
    var randomImages: [String]
    
    init(title: String, type: String, coordinateX: Int, coordinateY: Int, randomImages: [String]) {
        self.title = title
        self.type = type
        self.coordinateX = coordinateX
        self.coordinateY = coordinateY
        self.randomImages = randomImages
        self.image = randomImages[Int.random(in: 0..<randomImages.count)]
    }
}

We modified initialiser of NotAnimalObject for getting a random image any time when we initialise new object. 

We've done it. There is an example of the ObjectData file: 


import Foundation

protocol ObjectProtocol{
    var title: String {get}
    var type: String {get}
    var coordinateX: Int {get set}
    var coordinateY: Int {get set}
}

protocol AnimationProtocol {
    var animationImages: [String] {get}
    var animationDuration: Double {get}
}
protocol StaticImageProtocol {
    var image: String {get}
    var randomImages: [String] {get}
}


struct AnimalObject: ObjectProtocol, AnimationProtocol {
    let title: String
    let type: String
    var coordinateX: Int
    var coordinateY: Int
    var animationImages: [String]
    var animationDuration: Double
}

struct NotAnimalObject: ObjectProtocol, StaticImageProtocol{
    var title: String
    var type: String
    var coordinateX: Int
    var coordinateY: Int
    var image: String
    var randomImages: [String]
    
    init(title: String, type: String, coordinateX: Int, coordinateY: Int, randomImages: [String]) {
        self.title = title
        self.type = type
        self.coordinateX = coordinateX
        self.coordinateY = coordinateY
        self.randomImages = randomImages
        self.image = randomImages[Int.random(in: 0..<randomImages.count)]
    }
}

After setting up the model, we have to set up our ViewController which is connected to our scene in Main.storyboard. We create four outlets for three labels and one imageView and also we need to add the "touchUpInside" action for the button that invokes an object.

class ViewController: UIViewController {

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var typeLabel: UILabel!
    @IBOutlet weak var coordinatesLabel: UILabel!
    @IBOutlet weak var objectImageView: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func getObjectButtonTapped(_ sender: UIButton) {
    }
}

For the next step we need to add some images for our future objects. Images could be added into the Assets.xcassets folder.

Assets catalog Xcode

Now we need to initialise instances in our ViewController. Here the magic begins! The array objects has the type [ObjectProtocol], it means this array is able to contain all the properties that confirm to ObjectProtocol. You can throw objects array into "for in" loops, get all the properties and methods that contains ObjectProtocol and do many thing with this array. Nobody cares if you put different instances of types AnimalObject and NotAnimalObject, because both types confirm to ObjectProtocol.

    let objects: [ObjectProtocol] = [
       AnimalObject(title: "Bird", type: "Animal", coordinateX: 2, coordinateY: 0,
                     animationImages: ["frame-1", "frame-2", "frame-3", "frame-4",
                                      "frame-5", "frame-6", "frame-7", "frame-8"],
                     animationDuration: 0.7),
       AnimalObject(title: "Cat", type: "Animal", coordinateX: 1, coordinateY: 1,
                     animationImages: ["catrunx_1", "catrunx_2", "catrunx_3",
                                       "catrunx_4", "catrunx_5", "catrunx_6"],
                     animationDuration: 0.5),
       NotAnimalObject(title: "Stone", type: "Landscape", coordinateX: 0, coordinateY: 0,
                        randomImages: ["stone_0", "stone_1"])
       ]

Next we need to create a method that chooses random instance from objects array and updates labels and UIImageView according the chosen instance. This method should have two parts.

The first part includes getting an object's information from the properties that ObjectProtocol has. Every element of the objects array has the type ObjectProtocol that's why it is simple to get title, type and coordinates and also we don't need to downcast instance to AnimalObject and NotAnimalObject. We can, but we don't need.

private func updateUI(){
        
     //Working with the main ObjectProtocol, just getting text information.
        
     let object = objects[Int.random(in: 0..<objects.count)]
     titleLabel.text = object.title
     typeLabel.text = object.type
     coordinatesLabel.text = "X: \(object.coordinateX), Y: \(object.coordinateY)"
}

The second part applies simple multiple inheritance. It helps us to grab the images of objects from two superclasses protocols. We have different ways to display objects - displaying a static image or showing animation. Here we downcast our chosen instance to AnimationProtocol that gives us access to animationImages and animationDuration properties. If the instance doesn't confirm to AnimationProtolocol then it might confirm to StaticImageProtocol. From StaticImageProtocol we need only one property image, because we automatically randomised an image of the instance when initialised it. I hope, you remember.

private func updateUI(){
        
        //Working with the main ObjectProtocol, just getting text information.
        
        let object = objects[Int.random(in: 0..<objects.count)]
        titleLabel.text = object.title
        typeLabel.text = object.type
        coordinatesLabel.text = "X: \(object.coordinateX), Y: \(object.coordinateY)"
        
        /*Working with images protocols.
         Here we get an image or images of our random object by using downcasting
         to AnimationProtocol or StaticImageProtocol.*/
        
        if let animationObject = object as? AnimationProtocol{
            var animationImages: [UIImage] = []
            for image in animationObject.animationImages{
                animationImages.append(UIImage(named: image)!)
            }
            objectImageView.image = nil
            objectImageView.animationImages = animationImages
            objectImageView.animationDuration = animationObject.animationDuration
            objectImageView.startAnimating()
        }else if let staticImageObject = object as? StaticImageProtocol{
            if objectImageView.isAnimating{
                //cheking the object is doing animation after previous object
                objectImageView.stopAnimating()
            }
            objectImageView.image = UIImage(named: staticImageObject.image)
        }
    }

We set up objectImageView already. Now we need to add updateUI() everywhere we need to call it... Of course we put it into viewDidLoad() and getObjectButtonTapped(:_) methods:


    override func viewDidLoad() {
        super.viewDidLoad()
        updateUI()
    }

    @IBAction func getObjectButtonTapped(_ sender: UIButton) {
        updateUI()
    }

Finally, our ViewController should look like that:


import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var typeLabel: UILabel!
    @IBOutlet weak var coordinatesLabel: UILabel!
    @IBOutlet weak var objectImageView: UIImageView!
    
    let objects: [ObjectProtocol] = [
      AnimalObject(title: "Bird", type: "Animal", coordinateX: 2, coordinateY: 0,
                    animationImages: ["frame-1", "frame-2", "frame-3", "frame-4",
                                     "frame-5", "frame-6", "frame-7", "frame-8"],
                    animationDuration: 0.7),
      AnimalObject(title: "Cat", type: "Animal", coordinateX: 1, coordinateY: 1,
                    animationImages: ["catrunx_1", "catrunx_2", "catrunx_3",
                                      "catrunx_4", "catrunx_5", "catrunx_6"],
                    animationDuration: 0.5),
      NotAnimalObject(title: "Stone", type: "Landscape", coordinateX: 0, 
                    coordinateY: 0, randomImages: ["stone_0", "stone_1"])
      ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        updateUI()
    }

    @IBAction func getObjectButtonTapped(_ sender: UIButton) {
        updateUI()
    }
    
    private func updateUI(){
        
        //Working with the main ObjectProtocol, just getting text information.
        
        let object = objects[Int.random(in: 0..<objects.count)]
        titleLabel.text = object.title
        typeLabel.text = object.type
        coordinatesLabel.text = "X: \(object.coordinateX), Y: \(object.coordinateY)"
        
        /*Working with images protocols.
         Here we get an image or images of our random object by using downcasting
         to AnimationProtocol or StaticImageProtocol.*/
        
        if let animationObject = object as? AnimationProtocol{
            var animationImages: [UIImage] = []
            for image in animationObject.animationImages{
                animationImages.append(UIImage(named: image)!)
            }
            objectImageView.image = nil
            objectImageView.animationImages = animationImages
            objectImageView.animationDuration = animationObject.animationDuration
            objectImageView.startAnimating()
        }else if let staticImageObject = object as? StaticImageProtocol{
            if objectImageView.isAnimating{
                //cheking the object is doing animation after previous object
                objectImageView.stopAnimating()
            }
            objectImageView.image = UIImage(named: staticImageObject.image)
        }
    }
}

Launch the app and you will see everything works well! A bird is flying, a cat is running, a stone... a stone is a stone.

Animation of images in swift

There is a simple but very useful way of using Protocol Oriented Programming in Swift. I refactored thousands of lines of code in my iOS game, when I found out about POP.

Thanks for attention! If you have any ideas how to optimise this code, please share them in comments!




Thanks to Shepardskin for cat's animated sprites and thanks to Bevouliin.com for bird's animated sprites.

Thanks to Sphereion for the first stone picture and Pngimg.com for the second!

All the code has been highlighted by Swiftlighter.


Comments

Popular posts from this blog

We need to talk

Simple app for beginners. Nuked Code [0]