Thursday, August 4, 2016

Brief Introduction to iOS Development - Part 13 - Drawing Custom UI with Swift Programming

In this session, we will be creating custom UI control using Swift programming. We will be using an example from Apple tutorial "Start Developing iOS App". 

In this example, we will be creating a button in the shape of star. There will be five of them in a row. We can use such ratings system to rate photo.

In our example, we will be concentrating on creating such control. Now, lets create a single view project.


We named the project "Intro13". 


Once the project is created, go to the storyboard. Under object library, search for a control named as "View". Not any other view, just "View". See screenshot below.


Drag the view to the to left corner of the storyboard (x = 20 by y = 20).  We are not going to do auto-layout so the best location is top left corner. In addition, we need to set the size of the view to 240 x 44. We do so by changing the settings in size inspector. See settings below.


The resulting storyboard should be like the screen shot below.



Next, we need to create a new view file which is subclass from UIView. Create a new file from File >> New >> File...


Next, select iOS >> Source and on the right hand side select Cocoa Touch Class.


Under Class type in "RatingControl" and click Next.


Select the location. Usually we leave it as default. Click Create and a new file will be created. Check the screen shot below. 


After we had created the new file, we need to link this file to the View we just created. Go back to storyboard and select on the View.  Under Identity Inspector >> Custom Class. Changed class to RatingControl


Next we link the RatingControl view to the view controller file. we need to perform this task under assistant editor mode and we need to control+drag the view to the view controller. The settings is as follows:


The code of the link is as follows:


After the linking, we need to setup the star image. We can search the web for two star images or alternatively we can download this project from the Github and extract the image. We need to have two image, one white star represent empty star and a black star represent filled star. Go to Assets. 


Under Assets, create a new folder.


Named it as starRatings.


Next, we create two image set. The first set named as "filledStar". Then drag the black star to the middle of the box labeled 2x.



Next we named the other image set as "emptyStar" and drag the white star to the box labeled as 2x.


On the RatingControl file set up different categories as follows:


Under properties, create a variable named as "ratings" with property observer. A property observer is a set of action trigger when the value of the property changes. Under ratings, when the value is set it will trigger a program to perform the layout of this view again. 


    // MARK: Properties
    var ratings = 0 {
        didSet{
            setNeedsLayout()
        }
    }

The screen shot is as follows:


Next, we create other constants and variables as follows:

    // MARK: Properties
    var ratings = 0 {
        didSet{
            setNeedsLayout()
        }
    }
    var starCollection = [UIButton]()
    let totalStars = 5
    let spacing = 5

The screen shot is as follows:


Under the section initialization, we need to add an initializer for the class RatingControl. First type in init and the following screen will appear. Select init? as shown below.


The system will prompt an error with a red circle with white dot. 


Click on the red and white circle and the system will show error message with a solution labeled as "Fix-it"


Click on Fix-it and the system will add "required" in front of init?.


Now we need to perform the initialization under this section of code. First we need to initialize the super class using the following code:

super.init(coder: aDecoder)

The screen shot is as follows:


Next, we will create UIImage objects and link the UIImage to the two star image we have just inserted.  The code is as follows:

        // Create UIImage and load the image we prepared in Assets
        let emptyStarPic = UIImage(named: "emptyStar")
        let filledStarPic = UIImage(named: "filledStar")

The screen shot is as follows:


Next, we need to create UIButton using programming. Since we are going to create a number of them. It make sense to create in a loop.

        // Create 5 star button, set their initial image, add IBAction and add to star collection
        for _ in 0..<totalStars {

        }

The screen shot is as follows:


Next, we will create a UIButton named as starButton.

        // Create a UIButton known as starButton
        let starButton = UIButton()

The screen shot is as follows:


After creating the starButton, we can set image using the setImage function.

// Set the image for each starButton state
starButton.setImage(emptyStarPic, forState: .Normal)
starButton.setImage(filledStarPic, forState: .Selected)
starButton.setImage(filledStarPic, forState: [.Highlighted, .Selected])


Next we need to set a boolean adjustsImageWhenHighlighted to false. According to Apple, if the value is true, the starbutton when highlighted will be drawn lighter. I would suggest to set to false first. You can always change the setting after you have completed the program to see the effects.

// Set adjust image when highlighted to false. Apple Note: If true, the image is drawn lighter when the button is highlighted. The default value is true.
starButton.adjustsImageWhenHighlighted = false


Now, we have a slight detour. Under the category Action, create a function as follows:

    // MARK: Action
    @IBAction func starTapped(star:UIButton) {
        
        
    }


After we have create the function starTapped. We will go back to the initialization section and continue the coding after the adjustImageWhenHighlight section. Now we need to link the function starTapped we just created to starButton.


    // Link IBAction to each starbutton
    starButton.addTarget(self, action: #selector(RatingControl.starTapped(_:)),    forControlEvents: .TouchDown)


Next, we need to add the starButton in to the array.

    // Add starbutton to the UIbutton array
    starCollection += [starButton]


Finally, we need to addSubview. This function will draw the UIButton as a subview.

    // Add subview for starbutton to the view area
    addSubview(starButton)


The complete code for the initialization is as follows:

// MARK: Initialization
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        // Create UIImage and load the image we prepared in Assets
        let emptyStarPic = UIImage(named: "emptyStar")
        let filledStarPic = UIImage(named: "filledStar")
 
        
        // Create 5 star button, set their initial image, add IBAction and add to star collection
        for _ in 0..<totalStars {
            
            // Create a UIButton known as starButton
            let starButton = UIButton()
            
            // Set the image for each starButton state
            starButton.setImage(emptyStarPic, forState: .Normal)
            starButton.setImage(filledStarPic, forState: .Selected)
            starButton.setImage(filledStarPic, forState: [.Highlighted, .Selected])
            
            // Set adjust image when highlighted to false. Apple Note: If true, the image is drawn lighter when the button is highlighted. The default value is true.
            starButton.adjustsImageWhenHighlighted = false
 
            // Link IBAction to each starbutton
            starButton.addTarget(self, action: #selector(RatingControl.starTapped(_:)), forControlEvents: .TouchDown)
            
            // Add starbutton to the UIbutton array
            starCollection += [starButton]
            
            // Add subview for starbutton to the view area
            addSubview(starButton)
            
        }
    }


Screen shots below:


Next, we would like to override the function intrinsicContenSize. We need to override this function since the view has a specific size. The intrinsic size refers to the size of the whole view, which is 5 starButton aligning side by side.

To override the function, type override follow by the function name as follows:


Inside the function, we need to calculate the height and store the name as starSize. Since we have set the view earlier with a specific height, we can infer the height from the height of the frame. For total width, since the button is a square, the width of a single button is the same as height. For total width, we need to account for the width of 5 button including spacing. 

let starSize = Int(frame.size.height)
let totalWidth = totalStars * (starSize + spacing)


This function require us to return CGSize. Type in return CGSize and choose Int for data type as shown below.


Enter totalWdith for width and starSize for height.


The full code should be as follows:

    // We need to override this function so that we can set the size of 5 buttons
    override func intrinsicContentSize() -> CGSize {
        
        let starSize = Int(frame.size.height)
        let totalWidth = totalStars * (starSize + spacing)
        
        return CGSize(width: totalWidth, height: starSize)
    }


Now, we need to create another function just after starTapped. Type in func and the system will generate a function template.


Named the function as updateStarState with no arguments and return.


We will fill up the function code later. Now, we need to override another function under Layout. The function is layoutSubviews(). This function is to arrange the subview. We have added the subview(UIButton) in the initialization section. If we don't arrange the subview, all button will stage on top of each other. Override the layoutSubviews as follows:


We need to add the following code:

    // MARK: Layout
    
    // We need to override this function so that we can arrange all the star button side by side
    override func layoutSubviews() {
        
        let starSize = Int(frame.size.height)
        var starFrame = CGRect(x: 0, y: 0, width: starSize, height: starSize)
        
        // For each star in starCollection, we calculate the original x position of ech star. First star should be 0 and second star should be the size of the star plus spacing and multiple by 1 and so on...
        for (index, star) in starCollection.enumerate() {

            // Calculate original x position for each star
            starFrame.origin.x = CGFloat(index * (starSize + spacing))
            
            // Set the frame of each star to our calculated starFrame
            star.frame = starFrame
        }
        
        updateStarState()
        
    }


The first line is to setup the starSize and the second line is to setup the frame size of a button.

Then we need to iterate each button in the array. In the loop, we need to set original start point of x. The first button for x should be zero. The next should be 1 multiple by button size including spacing.

Finally, we set the frame of each starButton to the newly calculated frame. Only changes should be frame.origin.x.


Just after the loop, we need to add the updateStarState function as shown below:


Finally, we can add the code for the two function we created. For starTapped, it is easy, if user tap on the fifth button ratings should be 5. Since the array count from 0, we need to add 1 to the index. We also need the function to update the status of each button using the function update starState. In between the 2 statement, we need to add a print function for testing purpose.

    // MARK: Action
    @IBAction func starTapped(star:UIButton) {
        
        ratings = starCollection.indexOf(star)! + 1
        
        print(ratings)
        
        updateStarState()
        
    }


For update starState, we need to iterate each button. If ratings is 4, then all button with 4 and below should be marked as selected.

    func updateStarState()  {
        
        for (index, star) in starCollection.enumerate() {
            
            star.selected = index < ratings
        }
    }


Now, we can run and test the app. When we click on the button, a log section on the Xcode will appear. It will display the number for each button we click. Make that first button is 1 and so on. 


Once we know that the ratings is working fine. We can remove the statement. Usually, we will just comment it out as shown below so that we can always add the statement in for debugging purpose.


A copy of this program can be found at Github https://github.com/SwiftiCode/Intro13

*** End ***




No comments:

Post a Comment