Use notification observers to handle keyboard show and hide events:
// keyboard will show
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowAction(_:)), name: .UIKeyboardWillShow, object: nil)
// keyboard will hide
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHideAction(_:)), name: .UIKeyboardWillHide, object: nil)
You can do something like this when the keyboard is about to appear:
// keyboard will show action
@objc private func keyboardWillShowAction(_ notification: NSNotification) {
    currentScrollViewOffset = scrollView.contentOffset
    let keyboardFrame = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue
    let keyboardHeight = keyboardFrame?.cgRectValue.height
    let padding: CGFloat = 32
    // if text field view would be obstructed by keyboard, apply minimum-needed scroll view offset
    if activeTextFieldMaxYOnScreen! > (UIScreen.main.bounds.height - keyboardHeight! - padding) {
        let temporaryOffset = activeTextFieldMaxYOnScreen! - (UIScreen.main.bounds.height - (keyboardHeight! + padding))
        scrollView.setContentOffset(CGPoint(x: 0, y: currentScrollViewOffset.y + temporaryOffset), animated: true)
    }
    scrollView.addGestureRecognizer(tapToDismissKeyboard)
    scrollView.isScrollEnabled = false
}
And then revert when the keyboard is dismissed:
// keyboard will hide action
@objc private func keyboardWillHideAction(_ notification: NSNotification) {
    scrollView.setContentOffset(currentScrollViewOffset, animated: true)
    scrollView.removeGestureRecognizer(tapToDismissKeyboard)
    scrollView.isScrollEnabled = true
}
It's also good practice to remove observers in the deinit method:
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: nil)
Note that currentScrollViewOffset and activeTextFieldMaxYOnScreen are instance properties of the view controller. To get the activeTextFieldMaxYOnScreen value, you can get it from the text field delegate:
// text field should begin editing
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
    // convert text field line's max-y to perceived max-y on the screen
    let line = textField.viewWithTag(123)
    activeTextFieldMaxYOnScreen = (line?.convert((line?.bounds.origin)!, to: nil).y)! + (line?.frame.height)!
    return true
}