Tag: uikit

  • UIViewController Lifecycle Explained – loadView, viewDidLoad, viewWillAppear and More

    UIViewController Lifecycle Explained – loadView, viewDidLoad, viewWillAppear and More

    In this post, I’ll explain the life cycle of UIViewController.

    What happens when you create the UIViewController?

    1. loadView (only once called during the entire life cycles)

    2. viewDidLoad (only once called during the entire life cycles)

    3. viewWillAppear

    4. viewIsAppearing
    5. viewWillLayoutSubviews

    6. viewDidLayoutSubviews

    7. viewDidAppear

    And what happens when you dismiss or destroy the UIViewController?

    1. viewWillDisappear

    2. viewDidDisappear

    There are many steps of UIViewControllers. Let’s take a look loadView method.

    loadView

    Every view controller which subclassing the UIViewController has the view. It is the root view of the view controller. The default value is nil, and loadView is called when a view is a nil.

    There are three ways to initialize the UIViewControllers.

    1. Use Nib

    2. Use Storyboard

    3. Use Code

    You take care of loadView methods. If your view controller uses the Nib or Storyboard, then you should not override this method.

    Why?

    As I mentioned, You can initialize the UIViewController in different ways. If you use the Nib or Storyboard, the view will be set when loaded from Nib or Storyboard objects.

    Nib

    The init(nibName:bundle:) will set the view by loading the nib file.

    Storyboard

    The initiateViewController(withIdentifier:) will set the view. (UIStoryboard object create the view controller and return it to your code)

    Code

    You should override the loadView method to set the root view of the view controller. You can also put subviews into the root view on this method. Don’t call a super.loadView in the override loadView method.

    override func loadView() {
         let rootView = UIView()
         rootView.backgroundColor = .blue
         view = rootView
    
         //You can also add some more subviews into the view
     }

    ViewDidLoad

    It is called when the view controller’s view hierarchy is loaded into the memory.

    If you are using Storyboard or Nib, You can add, remove subviews on ViewDidLoad methods.

    Apple Document:

    Use this method to add or remove views, modify layout constraints, and load data for your views.

    viewWillAppear

    This method is called before configuring any animations and adding to the view hierarchy to display. You can set the style of view or set or change the orientation of the status bar. Call a super.viewWillAppear is required.

    viewWillLayoutSubviews

    When the view controller’s view is changed in its layout or bounds, then the system will call a viewWillLayoutSubviews. Compare to viewDidAppear; It will be automatically called when the view’s frame or bound changed, which means it can be called several times. On the other hand, viewDidAppear called once when the view controller’s view shows on the screen.

    roateView.gif

    Apple Document:

    Your view controller can override this method to make changes before the view lays out its subviews. The default implementation of this method does nothing.

    viewDidLayoutSubviews

    This method is called when after layout subViews. It means that the view is updated and lays out their bounds and frames. You can override this method if you need some implementation when after updated views. And You don’t need to call a super.viewDidLayoutSubviews because the default implementation does nothing.

    Apple Document:

    When the bounds change for a view controller’s view, the view adjusts the positions of its subviews and then the system calls this method. However, this method being called does not indicate that the individual layouts of the view’s subviews have been adjusted. Each subview is responsible for adjusting its own layout.

    updateViewConstraints

    If you use autolayout, You can override this method to update the view’s constraints. This method will be called when you call a view.setNeedsUpdateConstraints().

    What happens when you call a setNeedsUpdateConstraint?

    1. viewWillLayoutSubviews

    2. updateViewConstraints

    3. viewDidLayoutSubviews

    Here are examples

    @IBOutlet weak var buttonTopConstraint: NSLayoutConstraint!
    
    override func updateViewConstraints() {
        //Implement your code in here
        buttonTopConstraint.constant = 100
        //You should call a super.updateViewConstraints at final step
        super.updateViewConstraints()
    }

    Hmm, I watched the WWDC 15 video about this. I’m not sure should I need to override this function or not. Apple also recommends setting constraints in a place when you need to change it. (e.g., When tapped the button then update the constraint, set a constraint in an IBAction method) If you are facing a performance problem to update the constraints, then you can override updateViewConstraints.

    Apple Document:

    Override this method to optimize changes to your constraints.

    It is almost always cleaner and easier to update a constraint immediately after the affecting change has occurred. For example, if you want to change a constraint in response to a button tap, make that change directly in the button’s action method.

    You should only override this method when changing constraints in place is too slow, or when a view is producing a number of redundant changes.

    Your implementation must be as efficient as possible. Do not deactivate all your constraints, then reactivate the ones you need. Instead, your app must have some way of tracking your constraints, and validating them during each update pass. Only change items that need to be changed. During each update pass, you must ensure that you have the appropriate constraints for the app’s current state.

    viewDidAppear

    It is called when after all of the views in view controller are onScreen. Call a super.viewDidAppear is required.

    viewWillDisappear

    It is called the view being removed from the view hierarchy. You can use this function to resign the responder or revert the orientation or style of view set on viewWillAppear or viewDidAppear. Call a super.viewWillDisappear is required.

    viewDidDisapper

    It is called the view was removed from the view hierarchy. Use this function if you need some tasks when dismissing the view controller. Call a super.viewDidDisappear is required.

    References

    Apple Document, View Controllers

    Apple Document, View Controller Programming Guide for iOS

    WWDC 2015, Mysteries of Autolayout Part 2

  • iOS Tip Calculator Tutorial – UIKit and Storyboards Step-by-Step Guide

    iOS Tip Calculator Tutorial – UIKit and Storyboards Step-by-Step Guide

    In this tutorial, you’ll learn how to separate logics from view and to deal with user input on UITextField by making the calculator app. In this project, users can input the bill and tip, and it automatically displays the result of the total amount and tip amount when entering the bill or tip. You can also learn how to view the number of human-friendly formats.

    In the process, you’ll learn:

    • Designing the calculator app using storyboard and xib

    • Rendering the xib in storyboard by using IBDesignable and IBInspectable

    • Dealing with input using the UITextFieldDelegate and NumberFormatter

    • Reflect the result of the total amount and tip amount using the protocol.

    Key Topics

    • Storyboard

    • IBOutlet / IBAction

    • Delegate Pattern

    • NumberFormatter

     

    Designing the calculator app using storyboard and xib

    This calculator app has four components, which are two input fields and two result views. As you can see, the elements look similar. Isn’t it? It consists of UILabel, UITextField, and UIStepper. Ok, making the CustomView for a component using xib is the right choice rather than layout the all separated UILable, UITextField, and UIStepper 4 times into the Storyboard.

    Let’s create our InputView.

    ⌘ + N -> Source -> Swift File

    ⌘ + N -> User Interface -> View

    We created the Swift and Xib File. Let’s set up xib.

    widthConstraint.png

    We use the StackView to layout the item horizontally. The UILabel is set by width constraint relative to SuperView’s width and multiply the 0.15, which means UILabel’s width will place 15% of SuperView’s width.

    Look at Placeholders. The File’s Owner is pointing to InputView. The IBOutlet and IBAction are also linked to the InputView class. 

    Look at Placeholders. The File’s Owner is pointing to InputView. The IBOutlet and IBAction are also linked to the InputView class. 

    InputViewClass.png

    In the InputView.xib, open the Assistant for linking IBOutlet and the IBAction. When you link between xib component and InputView.swift, you will see the result like the image below.

    IBOutlet.png

    We almost did for setting InputView. Let’s write the code.

    import UIKit
    
    @IBDesignable
    final class InputView: UIView {
        @IBOutlet weak var titleLabel: UILabel!
        @IBOutlet weak var inputTextField: UITextField!
        @IBOutlet weak var tipStepper: UIStepper!
    
        override func prepareForInterfaceBuilder() {
            super.prepareForInterfaceBuilder()
        }
    
    //It called when you are using the xib for creating it.
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            setupView()
        }
    
    //It called when you create view programmatically.
        override init(frame: CGRect) {
            super.init(frame: frame)
            setupView()
        }
    
        private func setupView() {
            guard let nib = loadNib() else {
                return
            }
            nib.translatesAutoresizingMaskIntoConstraints = false
            addSubview(nib)
            NSLayoutConstraint.activate([
                nib.leadingAnchor.constraint(equalTo: self.leadingAnchor),
                nib.trailingAnchor.constraint(equalTo: self.trailingAnchor),
                nib.topAnchor.constraint(equalTo: self.topAnchor),
                nib.bottomAnchor.constraint(equalTo: self.bottomAnchor)
            ])
        }
    
        private func loadNib() -> UIView? {
            let bundle = Bundle(for: Self.self)
            return bundle.loadNibNamed(String(describing: Self.self), owner: self, options: nil)?.first as? UIView
        }
    
        @IBAction func didUpdateStepper(_ sender: UIStepper) {
            inputTextField.becomeFirstResponder()
            inputFormatter?.updateTipValue(inputTextField, value: sender.value)
        }
    }

    In the InputView.swift, We have two init function which is mandatory for making subView. To set subViews, we should load xib from Bundle, which organizes the code and resources in the Bundle directory. Then add subView into the InputView using autolayout.

    We use the @IBDesignable to load view on Storyboard. If not, the Storyboard can’t load our view. The @IBAction is aiming for handling the event like a tap, touchdown, and user actions. We linked the action from UIStepper to handle value changed event. 

     

    Rendering the xib in storyboard by using IBDesignable and IBInspectable

    As we already set @IBDesignable in InputView for displaying the view on the Storyboard. Let’s open the Storyboard to set the views.

    storyboard.png

    You can see our custom UIView called InputView is rendered on the Storyboard. Now I’ll explain the @IBInspectable for setting details on the Storyboard.

    Open the InputView.swift and add the @IBInspectable.

    @IBInspectable var title: String? = nil {
            didSet {
                titleLabel.text = title
            }
        }
    
        @IBInspectable var placeHolder: String? = nil {
            didSet {
                inputTextField.placeholder = placeHolder
            }
        }
    
        @IBInspectable var editable: Bool = true {
            didSet {
                inputTextField.isEnabled = editable
            }
        }
    
        @IBInspectable var tipInput: Bool = false {
            didSet {
                if tipInput {
                    tipStepper.isHidden = false
                }
            }
        }

    After adding the @IBInspectable var in InputView.swift, then open the Storyboard and check the InputView component.

    We added the four @IBInspectable, and you can set the values on the Storyboard. When you set the value, then It will reflect the value and rendering again.

    We added the four @IBInspectable, and you can set the values on the Storyboard. When you set the value, then It will reflect the value and rendering again.

     

    Dealing with input using the UITextFieldDelegate and NumberFormatter

    We will create the InputFormatter class to handle input from the keypad. To separating the logic from View, It handles the InputView’s UITextField event bypassing the delegate. 

    class InputFormatter: NSObject {
        enum InputType {
            case tip
            case bill
        }
    
        private var inputType: InputType = .bill
    
        private lazy var formatter: NumberFormatter = {
            let formatter = NumberFormatter()
            formatter.maximumFractionDigits = 0
            if self.inputType == .bill {
                formatter.numberStyle = .currency
                formatter.currencyCode = Locale(identifier: "en-US").currencyCode
            }
            else if self.inputType == .tip {
                formatter.numberStyle = .percent
                formatter.multiplier = 1
            }
            return formatter
        }()
    
        override init() {
            super.init()
        }
    
        convenience init(type: InputType) {
            self.init()
            inputType = type
        }
    
        private func setupKeyboardType(_ textField: UITextField) {
            textField.keyboardType = .numberPad
        }
    }
    
    extension InputFormatter: UITextFieldDelegate {    
        func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
            if textField.keyboardType == .default {
                setupKeyboardType(textField)
            }
            return true
        }
    
        func textField(
            _ textField: UITextField,
            shouldChangeCharactersIn range: NSRange,
            replacementString string: String
        ) -> Bool {
            guard let text = textField.text else {
                return true
            }
            if let formattedString = convertFormattedString(text, replacementString: string) {
                textField.text = formattedString
            }
            return false
        }
    }
    }

    This class has convenience init to handle inputType, whether tip or bill. For the tip, the NumberFormatter’s number style is percent to display the number with percentage symbol. For the bill, It will display the number with a currency symbol.

    Look at the UITextFieldDelegate functions. I handle the input stream from the number keypad and convert it for a human-readable format using the convertFormattedString function, which uses the NumberFormatter. (I attached full source code in this article. You can check it the function)

    func textField(
            _ textField: UITextField,
            shouldChangeCharactersIn range: NSRange,
            replacementString string: String
        ) -> Bool

    Let’s take a look above function. In this function, we can check the input string from the ‘replacementString’ parameter. It will be number because we set the keyboard type as numberPad. To use NumberFormatter to convert all input, we should return false to set the textField.text. 

    class ViewController: UIViewController {    
        @IBOutlet weak var resultStackView: UIStackView!
        @IBOutlet weak var resultTitleLabel: UILabel!
        @IBOutlet weak var tipResultView: InputView!
        @IBOutlet weak var totalResultView: InputView!
    
        @IBOutlet weak var inputStackView: UIStackView!
        @IBOutlet weak var inputTitleLabel: UILabel!
        @IBOutlet weak var billInputView: InputView!
        @IBOutlet weak var tipInputView: InputView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            resultStackView.setCustomSpacing(40, after: resultTitleLabel)
            inputStackView.setCustomSpacing(40, after: inputTitleLabel)
            billInputView.delegate = InputFormatter(type: .bill)
            tipInputView.delegate = InputFormatter(type: .tip)
        }
    }

    The ViewController code is pretty simple. You can just set the delegate in viewDidLoad.

     

    Reflect the result of the total amount and tip amount using the protocol

    app.png

    We are almost done. The final step reflects the result of an amount when entered bill or tip automatically. To do that, let’s create an AmountPresentable protocol and InputFormatterDelegate.

    protocol InputFormatterDelegate: AnyObject {
        func didUpdateInput(_ type: InputFormatter.InputType, number: NSNumber)
    }
    
    class InputFormatter: NSObject {
        private weak var delegate: InputFormatterDelegate?
    
    }

    We added the InputFormatterDelegate in InputFormatter and then conformed it in ViewController. It will let ViewController when detecting a typing event.

    To calculate the total amount and tip amount, We add AmountPresentable protocol.

    protocol AmountPresentable {
        var billValue: NSNumber { get }
        var tipValue: NSNumber { get }
        var tipPercentage: NSNumber { get }
        var formatter: NumberFormatter { get }
    
        func updateTipAmount(assign to: InputView)
        func updateTotalAmount(assign to: InputView)
        func updateStepperValue(assign to: InputView)
    }
    
    extension AmountPresentable {
        func updateTipAmount(assign to: InputView) {
            let tipAmount = formatter.string(from: self.tipValue)
            to.inputTextField.text = tipAmount
        }
    
        func updateStepperValue(assign to: InputView) {
            to.tipStepper.value = tipPercentage.doubleValue
        }
    
        func updateTotalAmount(assign to: InputView) {
            let totalValue = billValue.doubleValue + tipValue.doubleValue
            let totalAmount = formatter.string(from: NSNumber(value: totalValue))
            to.inputTextField.text = totalAmount
        }
    }

    This protocol binds to InputView to display the amount. It is called in the ViewController in the didUpdateAmount function. Let’s conform to the AmountPresentable on ViewController.

    class ViewController: UIViewController, AmountPresentable {
        var billValue: NSNumber = 0
        var tipValue: NSNumber = 0
        var tipPercentage: NSNumber = 0
        //…
    
        override func viewDidLoad() {
            super.viewDidLoad()
        //…
            billInputView.delegate = InputFormatter(type: .bill, delegate: self)
            tipInputView.delegate = InputFormatter(type: .tip, delegate: self)
        }
    }
    
    extension ViewController: InputFormatterDelegate {
        func didUpdateInput(_ type: InputFormatter.InputType, number: NSNumber) {
            if type == .tip {
                tipValue = NSNumber(value: billValue.doubleValue * (number.doubleValue * 0.01))
                tipPercentage = number
            }
            else if type == .bill {
                billValue = number
                tipValue = NSNumber(value: billValue.doubleValue * (tipPercentage.doubleValue * 0.01))
            }
            updateTipAmount(assign: tipResultView)
            updateTotalAmount(assign: totalResultView)
            updateStepperValue(assign: tipInputView)
        }
    }

    That’s all. When received the didUpdateInput function, then update the amount by using AmountPresentable. It enables us to reflect the amount during the enter the bill or tip.

     

    Conclusion

    We learned how to use Storyboard and Xib, UITextField, and Delegate patterns. We can improve our code logic if we use the combine framework, but It only supports iOS 13.0. If you understand this project and how delegation and protocol works, you can also quickly adapt the combined framework. I hope you enjoy this post. Thank you!

  • How to remove text insets on UITextView?

    How to remove text insets on UITextView?

    Default UITextView has text insets, unlike the UILabel.

    Default UITextView has text insets, unlike the UILabel.

    How to remove the insets on UITextView like an UILabel?

    UITextView

    UITextView

    On the storyboard, select the UITextView and remove the Scrolling Enabled.

    @IBOutlet weak var textView: UITextView!
    override func viewDidLoad() {
        super.viewDidLoad()
        textView.textContainer.lineFragmentPadding = 0
        textView.textContainerInset = .zero
    }

    removeInset.png

    UITextView which is removed text insets will look like an UILabel.

    Thanks for reviewing my post

    Divjjot Singh ( 신승훈 )

  • What’s new on scrollView?

    What’s new on scrollView?

    In Xcode 11, the scroll view has two new things, which are the Content Layout Guide and Frame Layout Guide.

    It’s a very convenient way to set content size for scrollView.

    Let’s check how to use it on the storyboard.

    scrollView.png

    What is the Content Layout Guide?

    It is the size of the content. For example, if your scrollView is enabled vertical scrolling only, then set the subviews constraints relative to the Content Layout Guide.

    viewConstraintStep1.png

    viewConstraintStep2.png

    What is the Frame Layout Guide?

    It is a fixed size of the content. For example, if scrollView is enabled vertical only, then just set the Frame Layout Guide’s width constraint and leave your height constraint.

    viewConstraintStep3.png

    How to resolve the constraint warnings?

    The above steps is an essential for scrollView with its subView. However Storyboard may still complain.

    constraintProblem.png

    To solve the constraint issue, change the intrinsic size of the scroll views subView.

    constraintSolve.png

    Add stackView

    For scrolling the contents, I added stackView with listViews.

    scrolling.gif

    Thanks for reviewing my post

    Shai Mishali

    Navati is having tea

    Bart Pang

  • How to render xib on Storyboard?

    How to render xib on Storyboard?

    When you create the custom view with xib and then set the custom view on Storyboard but if It has not appeared. How can you solve the rendering issue?

    I stuck in similar issues on Xcode 11.

    xcode 11 said -> Failed to render and update auto layout the agent threw an exception

    Here is my solution and I hope it helps you!

    customView.png

    import UIKit
    
    @IBDesignable
    class XibView: UIView {
        let className = String(describing: XibView.self)
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setupNib()
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setupNib()
        }
    
        private func setupNib() {
            guard let nib = loadNib() else { return }   
            nib.translatesAutoresizingMaskIntoConstraints = false
            addSubview(nib)
            NSLayoutConstraint.activate([
                nib.leadingAnchor.constraint(equalTo: self.leadingAnchor),
                nib.trailingAnchor.constraint(equalTo: self.trailingAnchor),
                nib.topAnchor.constraint(equalTo: self.topAnchor),
                nib.bottomAnchor.constraint(equalTo: self.bottomAnchor)
            ])
        }
    
        func loadNib() -> UIView? {
            let bundle = Bundle(for: Self.self)
            return bundle.loadNibNamed(String(describing: Self.self), owner: self, options: nil)?.first as? UIView
        }
    }

    image-asset.png