@@ -6,6 +6,8 @@ import UIKit
66
77@ main
88@ objc class AppDelegate : FlutterAppDelegate , FlutterImplicitEngineDelegate {
9+ private var keyboardInsets : PybleKeyboardInsets ?
10+
911 override func application (
1012 _ application : UIApplication ,
1113 didFinishLaunchingWithOptions launchOptions : [ UIApplication . LaunchOptionsKey : Any ] ?
@@ -17,6 +19,7 @@ import UIKit
1719 GeneratedPluginRegistrant . register ( with : engineBridge . pluginRegistry )
1820 guard let registrar = engineBridge . pluginRegistry . registrar ( forPlugin : " PybleKeyboardDismissal " )
1921 else { return }
22+ keyboardInsets = PybleKeyboardInsets ( registrar : registrar )
2023 let keyboard = FlutterMethodChannel (
2124 name : " dev.pyble/keyboard " , binaryMessenger : registrar . messenger ( ) )
2225 keyboard . setMethodCallHandler { [ weak registrar ] call , result in
@@ -31,3 +34,108 @@ import UIKit
3134 }
3235 }
3336}
37+
38+ /// Measures the current Flutter view's public layout guide instead of keyboard
39+ /// notification frames, which can be offscreen after an iPad palette transition.
40+ private final class PybleKeyboardInsets {
41+ private weak var registrar : FlutterPluginRegistrar ?
42+ private let channel : FlutterMethodChannel
43+ private var observer : PybleKeyboardGuideView ?
44+ private var generation : Int ?
45+ private var lastBottom : CGFloat ?
46+
47+ init ( registrar : FlutterPluginRegistrar ) {
48+ self . registrar = registrar
49+ channel = FlutterMethodChannel (
50+ name : " dev.pyble/keyboard/insets " , binaryMessenger : registrar . messenger ( ) )
51+ channel . setMethodCallHandler { [ weak self ] call , result in
52+ guard let self = self , let epoch = call . arguments as? Int else {
53+ result ( FlutterMethodNotImplemented )
54+ return
55+ }
56+ switch call . method {
57+ case " observe " :
58+ self . observe ( generation : epoch )
59+ result ( nil )
60+ case " stop " :
61+ if self . generation == epoch { self . stop ( ) }
62+ result ( nil )
63+ default :
64+ result ( FlutterMethodNotImplemented )
65+ }
66+ }
67+ }
68+
69+ private func observe ( generation : Int ) {
70+ if self . generation != generation {
71+ stop ( )
72+ self . generation = generation
73+ }
74+ guard let view = registrar ? . viewController ? . viewIfLoaded else {
75+ observer ? . removeFromSuperview ( )
76+ observer = nil
77+ publish ( 0 )
78+ return
79+ }
80+ if observer ? . superview !== view {
81+ observer ? . removeFromSuperview ( )
82+ let guideView = PybleKeyboardGuideView ( )
83+ guideView . translatesAutoresizingMaskIntoConstraints = false
84+ guideView . isUserInteractionEnabled = false
85+ guideView . accessibilityElementsHidden = true
86+ guideView . backgroundColor = . clear
87+ guideView . changed = { [ weak self ] bottom in self ? . publish ( bottom ) }
88+ observer = guideView
89+ view . addSubview ( guideView )
90+ NSLayoutConstraint . activate ( [
91+ guideView . topAnchor . constraint ( equalTo : view . keyboardLayoutGuide . topAnchor ) ,
92+ guideView . bottomAnchor . constraint ( equalTo : view . bottomAnchor ) ,
93+ guideView . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
94+ guideView . trailingAnchor . constraint ( equalTo : view . trailingAnchor ) ,
95+ ] )
96+ view . layoutIfNeeded ( )
97+ }
98+ // An explicit refresh must return a snapshot even if UIKit has not moved:
99+ // Dart may have rejected the previous value while its window size lagged.
100+ lastBottom = nil
101+ observer ? . reportOverlap ( )
102+ }
103+
104+ private func publish ( _ bottom : CGFloat ) {
105+ guard let generation = generation , bottom != lastBottom else { return }
106+ lastBottom = bottom
107+ channel . invokeMethod ( " inset " , arguments : [ " generation " : generation , " bottom " : bottom ] )
108+ }
109+
110+ private func stop ( ) {
111+ generation = nil
112+ observer ? . removeFromSuperview ( )
113+ observer = nil
114+ lastBottom = nil
115+ }
116+ }
117+
118+ private final class PybleKeyboardGuideView : UIView {
119+ var changed : ( ( CGFloat ) -> Void ) ?
120+
121+ override func layoutSubviews ( ) {
122+ super . layoutSubviews ( )
123+ reportOverlap ( )
124+ }
125+
126+ override func didMoveToWindow ( ) {
127+ super . didMoveToWindow ( )
128+ reportOverlap ( )
129+ }
130+
131+ func reportOverlap ( ) {
132+ guard let owner = superview , window != nil else {
133+ changed ? ( 0 )
134+ return
135+ }
136+ // On iOS 15/16 a hidden/undocked guide ends at the safe-area bottom.
137+ // A real docked keyboard or input palette reports its full overlap.
138+ let overlap = min ( owner . bounds . height , max ( 0 , bounds . height ) )
139+ changed ? ( overlap <= owner . safeAreaInsets . bottom + 0.5 ? 0 : overlap )
140+ }
141+ }
0 commit comments