UIButton can not display by using some specific iPhone devices models

I was trying to create a UIButton by code, and I want to add the UIButton as a subview into a UITextView.
Here is the code:

private func initTermsTextView() {

let filePath = Bundle.main.path(forResource: "terms", ofType: "html", inDirectory: nil, forLocalization: nil) ?? ""

let htmlString = try! String(contentsOfFile: filePath)

let htmlData = NSString(string: htmlString).data(using: String.Encoding.unicode.rawValue)

let options = [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html]

let attributedString = try! NSMutableAttributedString(data: htmlData!, options: options, documentAttributes: nil)

let font = UIFont(name: "BrandonGrotesque-Regular", size: 20) ?? UIFont.systemFont(ofSize: 20)

attributedString.setFontFace(font: font, color: UIColor.grayMetalic())

let btn: UIButton = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*11-135, width: 48, height: 48))

btn.setTitle("asdasdasdasda", for: .normal)

btn.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)

btn.clipsToBounds = true

btn.setImage(UIImage(named: "uncheckedBox"), for: .normal)

let path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*11-135, width: btn.frame.width, height: btn.frame.height))

self.termsTextView.textContainer.exclusionPaths = [path]


self.termsTextView.attributedText = attributedString

self.termsTextView.text.append(TermConditionSingleton.getInstance.setTCGetString())

self.termsTextView.addSubview(btn)


}


Since I have no idea how to add UIButton at the bottom of a UITextView, I did hardcore and count out the UITextView length(text length*11-135) and added the button under the UITextView.
After I was running my code with devices which is above the iPhone 6 (include 6s and plus) and the UIButton has displayed in the UITextView. But when I tried with iPhone 6 plus and iPhone 6s plus, the UIButton sub view did not show in the UITextView and I have no idea why.
Is there any way I can fix this type of problem?

Accepted Reply

It is always risky to hard code the device type…


If I look at the switch, they are all the same, except one constant, which is probably a number of lines.

Take care when you localize to a different language, that will change…

Have you check what happens when you rotate device ?


Anyway, in your present design, I would replace all

           btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: 48, height: 48))
           path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: btn!.frame.width, height: btn!.frame.height))

by a generic


           btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*nbLines - 262, width: 48, height: 48))
          path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*nbLines - 262, width: btn!.frame.width, height: btn!.frame.height))

with nbLines computed for each modelName


   var model: (name: String, lines: Int) { 
     // return the modelName if you still need it and the number of lines ; that will be easier to localize also
}


then, replace


       switch(UIDevice.current.modelName){
        case "iPhone 6 Plus":
            btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: 48, height: 48))
            path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: btn!.frame.width, height: btn!.frame.height))


by

          let nbLines = UIDevice.current.model.lines
          btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*nbLines - 262, width: 48, height: 48))
          path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*nbLines - 262, width: btn!.frame.width, height: btn!.frame.height))

Finally, you could create an UIButton subclass to define checkbox


In addition, nbLines probably just depends on the width of the textView (hence width of screen). Can't you compute it directly, as a function of this width ?

Replies

I think you'd better keep space at the bottom for the button, so that button doesn't move when you scroll.


May have a look here:

https://stackoverflow.com/questions/37674139/place-uibutton-at-the-end-of-text-in-uitextview-in-ios-app-in-xcode-written-in-s

Thanks for the reply Claude31 😀!
But I really want the button will be displayed as a subview when the user scrolls down the text view. The desired button layout I want is something like: " <checkbox> I accept the term&condition".
For the moment, I just hardcoded the button layout in different models since the term&condition text will not be changed so often.
I used an UIDevice extension to detect which device the user is

you can find the code at this link: https://stackoverflow.com/a/26962452/11055217

public extension UIDevice {
    
    /// pares the deveice name as the standard name
    var modelName: String {
        
        #if targetEnvironment(simulator)
        let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]!
        #else
        var systemInfo = utsname()
        uname(&systemInfo)
        let machineMirror = Mirror(reflecting: systemInfo.machine)
        let identifier = machineMirror.children.reduce("") { identifier, element in
            guard let value = element.value as? Int8 , value != 0 else { return identifier }
            return identifier + String(UnicodeScalar(UInt8(value)))
        }
        #endif
        
        switch identifier {
        case "iPod5,1":                                 return "iPod Touch 5"
        case "iPod7,1":                                 return "iPod Touch 6"
        case "iPhone3,1", "iPhone3,2", "iPhone3,3":     return "iPhone 4"
        case "iPhone4,1":                               return "iPhone 4s"
        case "iPhone5,1", "iPhone5,2":                  return "iPhone 5"
        case "iPhone5,3", "iPhone5,4":                  return "iPhone 5c"
        case "iPhone6,1", "iPhone6,2":                  return "iPhone 5s"
        case "iPhone7,2":                               return "iPhone 6"
        case "iPhone7,1":                               return "iPhone 6 Plus"
        case "iPhone8,1":                               return "iPhone 6s"
        case "iPhone8,2":                               return "iPhone 6s Plus"
        case "iPhone9,1", "iPhone9,3":                  return "iPhone 7"
        case "iPhone9,2", "iPhone9,4":                  return "iPhone 7 Plus"
        case "iPhone8,4":                               return "iPhone SE"
        case "iPhone10,1", "iPhone10,4":                return "iPhone 8"
        case "iPhone10,2", "iPhone10,5":                return "iPhone 8 Plus"
        case "iPhone10,3", "iPhone10,6":                return "iPhone X"
        case "iPhone11,2":                              return "iPhone XS"
        case "iPhone11,4", "iPhone11,6":                return "iPhone XS Max"
        case "iPhone11,8":                              return "iPhone XR"
        case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4":return "iPad 2"
        case "iPad3,1", "iPad3,2", "iPad3,3":           return "iPad 3"
        case "iPad3,4", "iPad3,5", "iPad3,6":           return "iPad 4"
        case "iPad4,1", "iPad4,2", "iPad4,3":           return "iPad Air"
        case "iPad5,3", "iPad5,4":                      return "iPad Air 2"
        case "iPad6,11", "iPad6,12":                    return "iPad 5"
        case "iPad7,5", "iPad7,6":                      return "iPad 6"
        case "iPad2,5", "iPad2,6", "iPad2,7":           return "iPad Mini"
        case "iPad4,4", "iPad4,5", "iPad4,6":           return "iPad Mini 2"
        case "iPad4,7", "iPad4,8", "iPad4,9":           return "iPad Mini 3"
        case "iPad5,1", "iPad5,2":                      return "iPad Mini 4"
        case "iPad6,3", "iPad6,4":                      return "iPad Pro 9.7 Inch"
        case "iPad6,7", "iPad6,8":                      return "iPad Pro 12.9 Inch"
        case "iPad7,1", "iPad7,2":                      return "iPad Pro (12.9-inch) (2nd generation)"
        case "iPad7,3", "iPad7,4":                      return "iPad Pro (10.5-inch)"
        case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4":return "iPad Pro (11-inch)"
        case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8":return "iPad Pro (12.9-inch) (3rd generation)"
        case "AppleTV5,3":                              return "Apple TV"
        case "AppleTV6,2":                              return "Apple TV 4K"
        case "AudioAccessory1,1":                       return "HomePod"
        default:                                        return identifier
        }
    }
    
}

Then I used a switch statement to check what model the user is using now:

private func initTermsTextView() {
        
        let filePath = Bundle.main.path(forResource: "terms", ofType: "html", inDirectory: nil, forLocalization: nil) ?? ""
        let htmlString = try! String(contentsOfFile: filePath)
        let htmlData = NSString(string: htmlString).data(using: String.Encoding.unicode.rawValue)
        let options = [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html]
        let attributedString = try! NSMutableAttributedString(data: htmlData!, options: options, documentAttributes: nil)
        let font = UIFont(name: "BrandonGrotesque-Regular", size: 20) ?? UIFont.systemFont(ofSize: 20)
        attributedString.setFontFace(font: font, color: UIColor.grayMetalic())
        
        print("Device type: \(UIDevice.current.modelName)")
        
        switch(UIDevice.current.modelName){
            
        case "iPhone 6 Plus":
            btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: 48, height: 48))
            path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: btn!.frame.width, height: btn!.frame.height))
            break
            
        case "iPhone 6s Plus":
            btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: 48, height: 48))
            path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: btn!.frame.width, height: btn!.frame.height))
            break
            
        case "iPhone XS Max":
            btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: 48, height: 48))
            path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: btn!.frame.width, height: btn!.frame.height))
            break
            
        case "iPhone XR":
            btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: 48, height: 48))
            path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: btn!.frame.width, height: btn!.frame.height))
            break
            
        case "iPhone 8 Plus":
            btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: 48, height: 48))
            path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: btn!.frame.width, height: btn!.frame.height))
            break
            
        case "iPhone 7 Plus":
            btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: 48, height: 48))
            path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: btn!.frame.width, height: btn!.frame.height))
            break
            
        case "iPhone 7":
            btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*16+133, width: 48, height: 48))
            path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*16+133, width: btn!.frame.width, height: btn!.frame.height))
            break
            
        default:
            btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*11-135, width: 48, height: 48))
            path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*11-135, width: btn!.frame.width, height: btn!.frame.height))
            break
        }
        
        btn!.setTitle("asdasdasdasda", for: .normal)
        btn!.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
        btn!.clipsToBounds = true
        btn!.setImage(UIImage(named: "uncheckedBox"), for: .normal)
        
        self.termsTextView.textContainer.exclusionPaths = [path] as! [UIBezierPath]
        
        self.termsTextView.attributedText = attributedString
        self.termsTextView.text.append(TermConditionSingleton.getInstance.setTCGetString())
        self.termsTextView.addSubview(btn!)
    }

This is not the best solution, but I would like to know if there is any better way to solve it.

It is always risky to hard code the device type…


If I look at the switch, they are all the same, except one constant, which is probably a number of lines.

Take care when you localize to a different language, that will change…

Have you check what happens when you rotate device ?


Anyway, in your present design, I would replace all

           btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: 48, height: 48))
           path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: btn!.frame.width, height: btn!.frame.height))

by a generic


           btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*nbLines - 262, width: 48, height: 48))
          path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*nbLines - 262, width: btn!.frame.width, height: btn!.frame.height))

with nbLines computed for each modelName


   var model: (name: String, lines: Int) { 
     // return the modelName if you still need it and the number of lines ; that will be easier to localize also
}


then, replace


       switch(UIDevice.current.modelName){
        case "iPhone 6 Plus":
            btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: 48, height: 48))
            path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*10-262, width: btn!.frame.width, height: btn!.frame.height))


by

          let nbLines = UIDevice.current.model.lines
          btn = UIButton(frame: CGRect(x: 8, y: termsTextView.contentSize.height*nbLines - 262, width: 48, height: 48))
          path = UIBezierPath(rect: CGRect(x: 8, y: termsTextView.contentSize.height*nbLines - 262, width: btn!.frame.width, height: btn!.frame.height))

Finally, you could create an UIButton subclass to define checkbox


In addition, nbLines probably just depends on the width of the textView (hence width of screen). Can't you compute it directly, as a function of this width ?

I know the risk is pretty high if you hardcode like this way and I can not find out how I'm going to solve this. But lucky, the application device orientation will be portrait and the T&C language will be only one for now.
I'm not sure if the nbLines is same as the value 10 in code "height*10", but I understand how your solution going to be. I will try to figure out how I can get the total text lines and make it fit for different IOS device.
Thank you so much Claude31 for the help! 😀