Repeating a texture over a plane in SceneKit

I faced an identical issue when implementing plane visualisation in ARKit. I wanted to visualise the detected plane as a checkerboard pattern. I fixed it by creating a custom SCNNode called a "PlaneNode" with a correctly configured SCNMaterial. The material uses wrapS, wrapT = .repeat and calculates the scale correctly based on the size of the plane itself.

Looks like this:

enter image description here

Have a look at the code below, the inline comments contain the explanation.

class PlaneNode : SCNNode {

    init(planeAnchor: ARPlaneAnchor) {
        super.init()
        // Create the 3D plane geometry with the dimensions reported
        // by ARKit in the ARPlaneAnchor instance
        let planeGeometry = SCNPlane(width:CGFloat(planeAnchor.extent.x), height:CGFloat(planeAnchor.extent.z))
        // Instead of just visualizing the grid as a gray plane, we will render
        // it in some Tron style colours.
        let material = SCNMaterial()
        material.diffuse.contents = PaintCode.imageOfViewARPlane
        //the scale gives the number of times the image is repeated
        //ARKit givest the width and height in meters, in this case we want to repeat
        //the pattern each 2cm = 0.02m so we divide the width/height to find the number of patterns
        //we then round this so that we always have a clean repeat and not a truncated one
        let scaleX = (Float(planeGeometry.width)  / 0.02).rounded()
        let scaleY = (Float(planeGeometry.height) / 0.02).rounded()
        //we then apply the scaling
        material.diffuse.contentsTransform = SCNMatrix4MakeScale(scaleX, scaleY, 0)
        //set repeat mode in both direction otherwise the patern is stretched!
        material.diffuse.wrapS = .repeat
        material.diffuse.wrapT = .repeat
        //apply material
        planeGeometry.materials = [material];
        //make a node for it
        self.geometry = planeGeometry
        // Move the plane to the position reported by ARKit
        position.x = planeAnchor.center.x
        position.y = 0
        position.z = planeAnchor.center.z
        // Planes in SceneKit are vertical by default so we need to rotate
        // 90 degrees to match planes in ARKit
        transform =  SCNMatrix4MakeRotation(-Float.pi / 2.0, 1.0, 0.0, 0.0);
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func update(planeAnchor: ARPlaneAnchor) {
        guard let planeGeometry = geometry as? SCNPlane else {
            fatalError("update(planeAnchor: ARPlaneAnchor) called on node that has no SCNPlane geometry")
        }
        //update the size
        planeGeometry.width = CGFloat(planeAnchor.extent.x)
        planeGeometry.height = CGFloat(planeAnchor.extent.z)
        //and material properties
        let scaleX = (Float(planeGeometry.width)  / 0.02).rounded()
        let scaleY = (Float(planeGeometry.height) / 0.02).rounded()
        planeGeometry.firstMaterial?.diffuse.contentsTransform = SCNMatrix4MakeScale(scaleX, scaleY, 0)
        // Move the plane to the position reported by ARKit
        position.x = planeAnchor.center.x
        position.y = 0
        position.z = planeAnchor.center.z

    }
}

I fixed it. It seems like the image was zoomed in. If I do imageMaterial.diffuse.contentsTransform = SCNMatrix4MakeScale(32, 32, 0), the image repeats.