Skip to content

Use the new API to obtain the interface orientation #2315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
991A97F72E1FB99300B47130 /* CMPScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 991A97F62E1FB99300B47130 /* CMPScrollView.m */; };
992EDDFB2E55EC8400FB44C5 /* CMPKeyValueObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 992EDDFA2E55EC8400FB44C5 /* CMPKeyValueObserver.m */; };
9968C35B2D76FE16005E8DE4 /* CMPPanGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 9968C35A2D76FE16005E8DE4 /* CMPPanGestureRecognizer.m */; };
9968C3612D7746BD005E8DE4 /* CMPHoverGestureHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9968C3602D7746BD005E8DE4 /* CMPHoverGestureHandler.m */; };
9968C38B2D7892DF005E8DE4 /* CMPScreenEdgePanGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 9968C38A2D7892DF005E8DE4 /* CMPScreenEdgePanGestureRecognizer.m */; };
Expand Down Expand Up @@ -66,6 +67,8 @@
/* Begin PBXFileReference section */
991A97F52E1FB99300B47130 /* CMPScrollView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CMPScrollView.h; sourceTree = "<group>"; };
991A97F62E1FB99300B47130 /* CMPScrollView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CMPScrollView.m; sourceTree = "<group>"; };
992EDDF92E55EC8400FB44C5 /* CMPKeyValueObserver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CMPKeyValueObserver.h; sourceTree = "<group>"; };
992EDDFA2E55EC8400FB44C5 /* CMPKeyValueObserver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CMPKeyValueObserver.m; sourceTree = "<group>"; };
9968C3592D76FE16005E8DE4 /* CMPPanGestureRecognizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CMPPanGestureRecognizer.h; sourceTree = "<group>"; };
9968C35A2D76FE16005E8DE4 /* CMPPanGestureRecognizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CMPPanGestureRecognizer.m; sourceTree = "<group>"; };
9968C35F2D7746BD005E8DE4 /* CMPHoverGestureHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CMPHoverGestureHandler.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -147,6 +150,8 @@
99D97A872BF73A9B0035552B /* CMPEditMenuView.m */,
991A97F52E1FB99300B47130 /* CMPScrollView.h */,
991A97F62E1FB99300B47130 /* CMPScrollView.m */,
992EDDF92E55EC8400FB44C5 /* CMPKeyValueObserver.h */,
992EDDFA2E55EC8400FB44C5 /* CMPKeyValueObserver.m */,
EA4B52942C2EDEF200FBB55C /* CMPGestureRecognizer.h */,
EA4B52952C2EDEF200FBB55C /* CMPGestureRecognizer.m */,
9968C35F2D7746BD005E8DE4 /* CMPHoverGestureHandler.h */,
Expand Down Expand Up @@ -351,6 +356,7 @@
99D97A882BF73A9B0035552B /* CMPEditMenuView.m in Sources */,
EABD912B2BC02B5F00455279 /* CMPInteropWrappingView.m in Sources */,
EADD02902C9846D9003F66E8 /* CMPDragInteractionProxy.m in Sources */,
992EDDFB2E55EC8400FB44C5 /* CMPKeyValueObserver.m in Sources */,
EA82F4F92B86144E00465418 /* CMPOSLogger.m in Sources */,
9968C3612D7746BD005E8DE4 /* CMPHoverGestureHandler.m in Sources */,
EA4B52962C2EDEF200FBB55C /* CMPGestureRecognizer.m in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import <Foundation/Foundation.h>

@interface CMPKeyValueObserver : NSObject

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import "CMPKeyValueObserver.h"

@implementation CMPKeyValueObserver

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import androidx.compose.ui.uikit.LocalUIViewController
import androidx.compose.ui.uikit.PlistSanityCheck
import androidx.compose.ui.uikit.density
import androidx.compose.ui.uikit.embedSubview
import androidx.compose.ui.uikit.utils.CMPKeyValueObserver
import androidx.compose.ui.uikit.utils.CMPViewController
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
Expand All @@ -65,6 +66,8 @@ import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.CPointed
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.CValue
import kotlinx.cinterop.ExportObjCClass
import kotlinx.coroutines.CoroutineScope
Expand All @@ -74,6 +77,9 @@ import org.jetbrains.skiko.OS
import org.jetbrains.skiko.OSVersion
import org.jetbrains.skiko.available
import platform.CoreGraphics.CGSize
import platform.Foundation.NSKeyValueObservingOptionNew
import platform.Foundation.addObserver
import platform.Foundation.removeObserver
import platform.UIKit.UIAccessibilityIsReduceMotionEnabled
import platform.UIKit.UIApplication
import platform.UIKit.UIStatusBarAnimation
Expand All @@ -83,6 +89,7 @@ import platform.UIKit.UIUserInterfaceLayoutDirection
import platform.UIKit.UIUserInterfaceStyle
import platform.UIKit.UIViewControllerTransitionCoordinatorProtocol
import platform.UIKit.UIWindow
import platform.UIKit.UIWindowScene
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_main_queue

Expand Down Expand Up @@ -112,6 +119,9 @@ internal class ComposeHostingViewController(
private var savableStateRegistry = SaveableStateRegistry(
restoredValues = null, canBeSaved = { true }
)
private val interfaceOrientationObserver = SceneGeometryObserver {
updateInterfaceOrientationState()
}

private val backGestureDispatcher = UIKitBackGestureDispatcher(
enableBackGesture = configuration.enableBackGesture,
Expand Down Expand Up @@ -142,15 +152,12 @@ internal class ComposeHostingViewController(
*/
private val currentInterfaceOrientation: InterfaceOrientation?
get() {
// Modern: https://developer.apple.com/documentation/uikit/uiwindowscene/3198088-interfaceorientation?language=objc
// Deprecated: https://developer.apple.com/documentation/uikit/uiapplication/1623026-statusbarorientation?language=objc
return InterfaceOrientation.getByRawValue(
if (available(OS.Ios to OSVersion(13))) {
view.window?.windowScene?.interfaceOrientation
?: UIApplication.sharedApplication.statusBarOrientation
if (available(OS.Ios to OSVersion(16))) {
view.window?.windowScene?.effectiveGeometry?.interfaceOrientation
} else {
UIApplication.sharedApplication.statusBarOrientation
}
view.window?.windowScene?.interfaceOrientation
} ?: UIApplication.sharedApplication.statusBarOrientation
)
}

Expand Down Expand Up @@ -201,6 +208,8 @@ internal class ComposeHostingViewController(

private fun onDidMoveToWindow(window: UIWindow?) {
backGestureDispatcher.onDidMoveToWindow(window, rootView)
interfaceOrientationObserver.windowScene = window?.windowScene

val windowContainer = window ?: return

updateInterfaceOrientationState()
Expand All @@ -220,7 +229,6 @@ internal class ComposeHostingViewController(
interfaceOrientationState.value = orientation
}


override fun viewWillTransitionToSize(
size: CValue<CGSize>,
withTransitionCoordinator: UIViewControllerTransitionCoordinatorProtocol
Expand Down Expand Up @@ -332,6 +340,7 @@ internal class ComposeHostingViewController(
updateMotionSpeed()
}
}
interfaceOrientationObserver.isObservingEnabled = true

backGestureDispatcher.onDidMoveToWindow(view.window, rootView)
onAccessibilityChanged()
Expand Down Expand Up @@ -359,6 +368,8 @@ internal class ComposeHostingViewController(

layersHolder?.disposeIfNeeded()
layersHolder = null

interfaceOrientationObserver.isObservingEnabled = false
}

@OptIn(NativeRuntimeApi::class)
Expand Down Expand Up @@ -560,3 +571,52 @@ private class ComposeLayersHolder(
layersViewController = null
}
}

private class SceneGeometryObserver(
val onGeometryChanged: () -> Unit
): CMPKeyValueObserver() {
private val observingKey = "effectiveGeometry"

var windowScene: UIWindowScene? = null
set(value) {
if (field == value) return
removeObserverIfNeeded()
field = value
addObserverIfNeeded()
}

var isObservingEnabled = false
set(value) {
if (field == value) return
field = value
if (value) {
addObserverIfNeeded()
} else {
removeObserverIfNeeded()
}
}

private var isObservingAdded = false

private fun addObserverIfNeeded() {
if (isObservingEnabled && !isObservingAdded) {
isObservingAdded = true
windowScene?.addObserver(this, observingKey, NSKeyValueObservingOptionNew, null)
}
}

private fun removeObserverIfNeeded() {
windowScene?.removeObserver(this, observingKey)
isObservingAdded = false
}

override fun observeValueForKeyPath(
keyPath: String?,
ofObject: Any?,
change: Map<Any?, *>?,
context: CPointer<out CPointed>?
) {
onGeometryChanged()
}
}