Skip to content

Commit

Permalink
Add flag to disable EpoxySwiftUIHostingController keyboard avoidance (
Browse files Browse the repository at this point in the history
#170)

* Add flag to disable `EpoxySwiftUIHostingController` keyboard avoidance

* Update Xcode CI versions

* Update Xcode CI versions again

* Use `ignores` instead of `ignore`

* Update CI simulator version

* Run swiftformat
  • Loading branch information
sammygutierrez authored Jul 23, 2024
1 parent 158193d commit a1a2929
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 13 deletions.
14 changes: 7 additions & 7 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ jobs:
strategy:
matrix:
xcode:
- '13.2.1' # Swift 5.5 (lowest)
- '14.0.1' # Swift 5.7 (highest)
- '14.3.1' # Swift 5.8 (lowest)
- '15.4' # Swift 5.10 (highest)
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/setup
Expand All @@ -27,7 +27,7 @@ jobs:
strategy:
matrix:
xcode:
- '14.0.1' # Swift 5.7 (highest)
- '15.4' # Swift 5.10 (highest)
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/setup
Expand All @@ -41,8 +41,8 @@ jobs:
strategy:
matrix:
xcode:
- '13.2.1' # Swift 5.5 (lowest)
- '14.0.1' # Swift 5.7 (highest)
- '14.3.1' # Swift 5.8 (lowest)
- '15.4' # Swift 5.10 (highest)
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/setup
Expand All @@ -56,8 +56,8 @@ jobs:
strategy:
matrix:
xcode:
- '13.2.1' # Swift 5.5 (lowest)
- '14.0.1' # Swift 5.7 (highest)
- '14.3.1' # Swift 5.8 (lowest)
- '15.4' # Swift 5.10 (highest)
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/setup
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Made new layout-based SwiftUI cell rendering option the default.
- Fixed interaction of SwiftUI bars on visionOS
- Added flag for forcing layout on a hosted SwiftUI view after layout margins change
- Updated `EpoxySwiftUIHostingController` with a flag to disable its keyboard avoidance behavior

## [0.10.0](https://github.com/airbnb/epoxy-ios/compare/0.9.0...0.10.0) - 2023-06-29

Expand Down
6 changes: 3 additions & 3 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace :build do

desc 'Builds the EpoxyExample app'
task :example do
xcodebuild 'build -scheme EpoxyExample -destination "platform=iOS Simulator,name=iPhone 12"'
xcodebuild 'build -scheme EpoxyExample -destination "platform=iOS Simulator,name=iPhone 14"'
end
end

Expand All @@ -23,12 +23,12 @@ namespace :test do

desc 'Runs unit tests'
task :unit do
xcodebuild 'test -scheme EpoxyTests -destination "platform=iOS Simulator,name=iPhone 12"'
xcodebuild 'test -scheme EpoxyTests -destination "platform=iOS Simulator,name=iPhone 14"'
end

desc 'Runs performance tests'
task :performance do
xcodebuild 'test -scheme PerformanceTests -destination "platform=iOS Simulator,name=iPhone 12"'
xcodebuild 'test -scheme PerformanceTests -destination "platform=iOS Simulator,name=iPhone 14"'
end
end

Expand Down
56 changes: 54 additions & 2 deletions Sources/EpoxyCore/SwiftUI/EpoxySwiftUIHostingController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@ open class EpoxySwiftUIHostingController<Content: View>: UIHostingController<Con

/// Creates a `UIHostingController` that optionally ignores the `safeAreaInsets` when laying out
/// its contained `RootView`.
public convenience init(rootView: Content, ignoreSafeArea: Bool) {
public convenience init(rootView: Content, ignoresSafeArea: Bool, ignoresKeyboardAvoidance: Bool) {
self.init(rootView: rootView)

// We unfortunately need to call a private API to disable the safe area. We can also accomplish
// this by dynamically subclassing this view controller's view at runtime and overriding its
// `safeAreaInsets` property and returning `.zero`. An implementation of that logic is
// available in this file in the `2d28b3181cca50b89618b54836f7a9b6e36ea78e` commit if this API
// no longer functions in future SwiftUI versions.
_disableSafeArea = ignoreSafeArea
_disableSafeArea = ignoresSafeArea

if ignoresKeyboardAvoidance {
disableKeyboardAvoidance()
}
}

// MARK: Open
Expand All @@ -41,5 +45,53 @@ open class EpoxySwiftUIHostingController<Content: View>: UIHostingController<Con
// below, e.g. to draw highlight states in a `CollectionView`.
view.backgroundColor = .clear
}

// MARK: Private

/// Creates a dynamic subclass of this hosting controller's view that disables its keyboard
/// avoidance behavior.
/// Setting `safeAreaRegions` to `.container` also works but cannot be used since it's
/// supported on 16.4+ and we need to support older versions.
/// See [here](https://steipete.com/posts/disabling-keyboard-avoidance-in-swiftui-uihostingcontroller/) for more info.
private func disableKeyboardAvoidance() {
guard let viewClass = object_getClass(view) else {
EpoxyLogger.shared.assertionFailure("Unable to determine class of \(String(describing: view))")
return
}

let viewClassName = class_getName(viewClass)
let viewSubclassName = String(cString: viewClassName).appending("_IgnoresKeyboard")

// If subclass already exists, just set the class of `view` and return.
if let subclass = NSClassFromString(viewSubclassName) {
object_setClass(view, subclass)
return
}

guard let viewSubclassNameUTF8 = (viewSubclassName as NSString).utf8String else {
EpoxyLogger.shared.assertionFailure("Unable to get utf8String of \(viewSubclassName)")
return
}
guard let viewSubclass = objc_allocateClassPair(viewClass, viewSubclassNameUTF8, 0) else {
EpoxyLogger.shared.assertionFailure(
"Unable to subclass \(viewClass) with \(viewSubclassNameUTF8)")
return
}

let selector = NSSelectorFromString("keyboardWillShowWithNotification:")
guard let method = class_getInstanceMethod(viewClass, selector) else {
EpoxyLogger.shared.assertionFailure("Unable to locate method \(selector) on \(viewClass)")
objc_disposeClassPair(viewSubclass)
return
}

let keyboardWillShowOverride: @convention(block) (AnyObject, AnyObject) -> Void = { _, _ in }
let implementation = imp_implementationWithBlock(keyboardWillShowOverride)
let typeEncoding = method_getTypeEncoding(method)
class_addMethod(viewSubclass, selector, implementation, typeEncoding)

objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
#endif
3 changes: 2 additions & 1 deletion Sources/EpoxyCore/SwiftUI/EpoxySwiftUIHostingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ public final class EpoxySwiftUIHostingView<RootView: View>: UIView, EpoxyableVie
epoxyContent = EpoxyHostingContent(rootView: style.initialContent.rootView)
viewController = EpoxySwiftUIHostingController(
rootView: .init(content: epoxyContent, environment: epoxyEnvironment),
ignoreSafeArea: true)
ignoresSafeArea: true,
ignoresKeyboardAvoidance: true)

dataID = style.initialContent.dataID ?? DefaultDataID.noneProvided as AnyHashable
forceLayoutOnLayoutMarginsChange = style.forceLayoutOnLayoutMarginsChange
Expand Down

0 comments on commit a1a2929

Please sign in to comment.