Search code examples
swiftuiviewuibezierpathtic-tac-toe

how to draw tic tac toe board as UIView in swift?


In my UIViewController I have a single subclass of UIView in which I will draw a tic tac toe board. Somehow the dividers (the "#" shape) I'm drawing using UIBezierPath() are not dividing the board evenly. Instead of 1/3-1/3-1/3, the vertical dividers are closer to 45%-45%-10% even though the dimensions printouts make sense. What am I missing? Thanks

In my subclass:

import UIKit

@IBDesignable class GameBoardView: UIView {

    override func drawRect(rect: CGRect) {

        // set up gameBoard dimensions everytime drawRect() is called
        setUpGameBoardCells()
        self.frame = CGRectMake(gameBoardPosX, gameBoardPosY, gameBoardLength, gameBoardLength)
        print("gameBoard.frame: x=\(self.frame.origin.x), y=\(self.frame.origin.y), h=\(self.frame.height), w=\(self.frame.width)\n")

        // draw dividers & cells
        var divider = UIBezierPath(rect: CGRect(x: cellWidth, y: 0, width: dividerWidth, height: gameBoardLength))
        divider.lineWidth = 1
        UIColor.orangeColor().setStroke()
        divider.stroke()

        divider = UIBezierPath(rect: CGRect(x: cellWidth * 2 + dividerWidth, y: 0, width: dividerWidth, height: gameBoardLength))
        divider.stroke()
   }
}

And this is how I set up the dimensions to handle any sized screens:

var screenSize = CGRect()
let screenMargin: CGFloat = 20   // to the edge

var gameBoardIsPortrait = Bool()
var gameBoardLength = CGFloat()
var gameBoardPosX = CGFloat()
var gameBoardPosY = CGFloat()

let cellsPerRow: Int = 3
var cellWidth = CGFloat()
let dividerWidthGuide: CGFloat = 0.02   // guideline % of gameBoardLength
var dividerWidth = CGFloat()

let debugPrint = true

func setUpGameBoardCells() {

    screenSize = UIScreen.mainScreen().bounds

    // gameBoard is a square
    gameBoardIsPortrait = (screenSize.height >= screenSize.width ? true : false)
    gameBoardLength = min(screenSize.height, screenSize.width) - screenMargin * 2
    gameBoardPosX = (screenSize.width - gameBoardLength) / 2
    gameBoardPosY = (screenSize.height - gameBoardLength) / 2

    // want cells & dividers on gameBoard to be whole numbers
    dividerWidth = round(gameBoardLength * dividerWidthGuide)
    let cellsTotalWidth: Int = Int(gameBoardLength) - Int(dividerWidth) * (cellsPerRow - 1)
    let dividerWidthFudge: CGFloat = (cellsTotalWidth % cellsPerRow == 1 ? -1 : (cellsTotalWidth % cellsPerRow == 2 ? 1 : 0))
    dividerWidth += dividerWidthFudge
    cellWidth = CGFloat((cellsTotalWidth - Int(dividerWidthFudge) * (cellsPerRow - 1)) / cellsPerRow)

    if debugPrint {
        print("setUpCellDivision()->\nscreen: h=\(screenSize.height), w=\(screenSize.width)")
        print("gameBoardIsPortrait=\(gameBoardIsPortrait), gameBoardLength=\(gameBoardLength), gameBoardPosX=\(gameBoardPosX), gameBoardPosY=\(gameBoardPosY)")
        print("cellWidth=\(cellWidth), dividerWidth=\(dividerWidth)\n")
    }
}

What is bizarre is that in xcode it looks right:

enter image description here

But in simulator it looks like this:

enter image description here


Solution

  • The problem is the setting of the frame from within drawRect. This would especially be a problem if you have any auto-layout constraints defined for the view.

    The laying out of a view and the drawing of that view are two different steps, and you should therefore separate that logic.

    Personally, I'd set up auto-layout constraints on the view to make sure that it is square, centered, and had the correct spacing. Then the view rendering is simplified:

    @IBDesignable class GameBoardView: UIView {
    
        override func drawRect(rect: CGRect) {
            setUpGameBoardCells()
    
            UIColor.orangeColor().setStroke()
    
            var divider = UIBezierPath(rect: CGRect(x: cellWidth, y: 0, width: dividerWidth, height: bounds.size.height))
            divider.lineWidth = 1
            divider.stroke()
    
            divider = UIBezierPath(rect: CGRect(x: cellWidth * 2 + dividerWidth, y: 0, width: dividerWidth, height: bounds.size.height))
            divider.lineWidth = 1
            divider.stroke()
        }
    
        let cellsPerRow = 3
        let dividerWidthGuide: CGFloat = 0.02   // guideline % of gameBoardLength
    
        var cellWidth: CGFloat!
        var cellHeight: CGFloat!
        var dividerWidth: CGFloat!
    
        func setUpGameBoardCells() {
            let gameBoardLength = min(bounds.size.height, bounds.size.width)
            dividerWidth = round(gameBoardLength * dividerWidthGuide)
            let cellsTotalWidth: Int = Int(gameBoardLength) - Int(dividerWidth) * (cellsPerRow - 1)
            let dividerWidthFudge: CGFloat = (cellsTotalWidth % cellsPerRow == 1 ? -1 : (cellsTotalWidth % cellsPerRow == 2 ? 1 : 0))
            dividerWidth! += dividerWidthFudge
            cellWidth = CGFloat((cellsTotalWidth - Int(dividerWidthFudge) * (cellsPerRow - 1)) / cellsPerRow)
        }
    
    }
    

    That yields:

    enter image description here

    Clearly, just repeat for your horizontal separators, too.