Skip to content

Commit

Permalink
Add support to KeyboardLayoutGuide
Browse files Browse the repository at this point in the history
  • Loading branch information
dogo committed Sep 21, 2024
1 parent 1e05618 commit dbc3630
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 1 deletion.
5 changes: 5 additions & 0 deletions Sources/SketchKit/Constrainable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ extension View: Constrainable {
public var layout: SketchKitDSL {
return SketchKitDSL(constrainable: self)
}

/// A layout guide that dynamically adjusts to the keyboard's frame.
public var keyboardLayoutGuide: LayoutGuide? {
return SketchKitDSL(constrainable: self).resolveKeyboardLayoutGuide()
}
}

/// LayoutGuide is an abstraction of Apple layout system
Expand Down
135 changes: 135 additions & 0 deletions Sources/SketchKit/KeyboardLayoutGuide.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//
// KeyboardLayoutGuide.swift
// SketchKit
//
// Created by Diogo Autilio on 04/10/19.
// Copyright (c) 2021 Anykey Entertrainment. All rights reserved.
//

import Foundation
import UIKit

final class SKKeyboard {
static let shared = SKKeyboard()
var currentHeight: CGFloat = 0

private init() {
// Safe singleton
}
}

final class KeyboardLayoutGuide: LayoutGuide {

private var bottomConstraint: NSLayoutConstraint?

init(notificationCenter: NotificationCenter = NotificationCenter.default) {
super.init()
// Observe keyboardWillChangeFrame notifications
notificationCenter.addObserver(self,
selector: #selector(adjustKeyboard(_:)),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil)

// Observe keyboardWillHide notifications
notificationCenter.addObserver(self,
selector: #selector(adjustKeyboard(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil)
}

@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

deinit {
NotificationCenter.default.removeObserver(self)
}

func setUp() {
guard let owningView else { return }

self.layout.applyConstraint {
$0.heightAnchor(equalToConstant: SKKeyboard.shared.currentHeight)
$0.leftAnchor(equalTo: owningView.leftAnchor)
$0.rightAnchor(equalTo: owningView.rightAnchor)
}

updateBottomAnchor()
}

func updateBottomAnchor() {
guard let owningView else { return }

bottomConstraint?.isActive = false
bottomConstraint = bottomAnchor.constraint(equalTo: owningView.safeBottomAnchor)
bottomConstraint?.isActive = true
}

@objc
private func adjustKeyboard(_ notification: Notification) {
if var height = notification.keyboardHeight, let duration = notification.animationDuration {
if height > 0, let bottom = owningView?.safeAreaInsets.bottom {
height -= bottom
}
heightConstraint?.constant = height
if duration > 0.0 {
animate(notification)
}
SKKeyboard.shared.currentHeight = height
}
}

private func animate(_ notification: Notification) {
if let owningView, isVisible(view: owningView) {
owningView.layoutIfNeeded()
} else {
UIView.performWithoutAnimation { [weak self] in
self?.owningView?.layoutIfNeeded()
}
}
}

private func isVisible(view: UIView) -> Bool {
func isVisible(view: UIView, inView: UIView?) -> Bool {
guard let inView else { return true }
let viewFrame = inView.convert(view.bounds, from: view)
if viewFrame.intersects(inView.bounds) {
return isVisible(view: view, inView: inView.superview)
}
return false
}
return isVisible(view: view, inView: view.superview)
}
}

// MARK: - Helpers

extension UILayoutGuide {
internal var heightConstraint: NSLayoutConstraint? {
return owningView?.constraints.first {
$0.firstItem as? UILayoutGuide == self && $0.firstAttribute == .height
}
}
}

extension Notification {
var keyboardHeight: CGFloat? {
guard let keyboardFrame = userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
return nil
}

if name == UIResponder.keyboardWillHideNotification {
return 0.0
} else {
// Weirdly enough UIKeyboardFrameEndUserInfoKey doesn't have the same behaviour
// in ios 10 or iOS 11 so we can't rely on v.cgRectValue.width
let screenHeight = UIApplication.shared.keyWindow?.bounds.height ?? UIScreen.main.bounds.height
return screenHeight - keyboardFrame.cgRectValue.minY
}
}

var animationDuration: CGFloat? {
return self.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? CGFloat
}
}
21 changes: 20 additions & 1 deletion Sources/SketchKit/SketchKitDSL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public typealias EdgeInsets = UIEdgeInsets
/// SketchKitDSL
public class SketchKitDSL {

let constrainable: Constrainable
private let constrainable: Constrainable

init(constrainable: Constrainable) {
self.constrainable = constrainable
Expand All @@ -55,4 +55,23 @@ public class SketchKitDSL {
}
block(self.constrainable)
}

func resolveKeyboardLayoutGuide() -> LayoutGuide? {
guard let view = constrainable as? View else { return nil }

let layoutGuideIdentifier = "KeyboardLayoutGuide"

// Attempt to find an existing keyboard layout guide by its identifier
if let existingLayoutGuide = view.layoutGuides.first(where: { $0.identifier == layoutGuideIdentifier }) {
return existingLayoutGuide
}

// Create and configure a new keyboard layout guide if it doesn't exist
let keyboardLayoutGuide = KeyboardLayoutGuide()
keyboardLayoutGuide.identifier = layoutGuideIdentifier
view.addLayoutGuide(keyboardLayoutGuide)
keyboardLayoutGuide.setUp()

return keyboardLayoutGuide
}
}

0 comments on commit dbc3630

Please sign in to comment.