Skip to content

Commit dca9d63

Browse files
authored
Refactor compose container lifecycle delegate (#2580)
Preliminary refactoring before the https://youtrack.jetbrains.com/issue/CMP-8478/Implement-UIView-based-compose-injection-API - Rename `CMPViewControllerLifecycleDelegate` to `CMPComposeContainerLifecycleDelegate` as well as corresponding `ComposeContainerLifecycleDelegate` in Kotlin code. - Implement UIView-based superclass `CMPView` that able to track visibility based on superview + window combination, similar to the `CMPViewController`. - Exposes Obj-C interface `userInterfaceStyleDidChange` to monitor changes for various iOS versions ## Release Notes N/A
1 parent 3c5b1cf commit dca9d63

File tree

13 files changed

+515
-86
lines changed

13 files changed

+515
-86
lines changed

compose/ui/ui-uikit/src/uikitMain/objc/CMPUIKitUtils/CMPUIKitUtils.xcodeproj/project.pbxproj

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
997DFCF32B18DE59000B56B5 /* MockAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DFCF22B18DE59000B56B5 /* MockAppDelegate.swift */; };
2020
997DFCF52B18E276000B56B5 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DFCF42B18E276000B56B5 /* XCTestCase.swift */; };
2121
997DFCFD2B18E5D3000B56B5 /* CMPUIKitUtilsTestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DFCFC2B18E5D3000B56B5 /* CMPUIKitUtilsTestApp.swift */; };
22+
99CC4B2E2ECE0838007C5C44 /* CMPView.m in Sources */ = {isa = PBXBuildFile; fileRef = 99CC4B2D2ECE0838007C5C44 /* CMPView.m */; };
23+
99CC4B2F2ECE0838007C5C44 /* CMPView.m in Sources */ = {isa = PBXBuildFile; fileRef = 99CC4B2D2ECE0838007C5C44 /* CMPView.m */; };
24+
99CC4B302ECE0838007C5C44 /* CMPView.m in Sources */ = {isa = PBXBuildFile; fileRef = 99CC4B2D2ECE0838007C5C44 /* CMPView.m */; };
25+
99CC4B322ECE16C8007C5C44 /* CMPViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99CC4B312ECE16C8007C5C44 /* CMPViewTests.swift */; };
2226
99D97A882BF73A9B0035552B /* CMPEditMenuView.m in Sources */ = {isa = PBXBuildFile; fileRef = 99D97A872BF73A9B0035552B /* CMPEditMenuView.m */; };
2327
99DCAB0E2BD00F5C002E6AC7 /* CMPTextLoupeSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 99DCAB0D2BD00F5C002E6AC7 /* CMPTextLoupeSession.m */; };
2428
EA4B52962C2EDEF200FBB55C /* CMPGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = EA4B52952C2EDEF200FBB55C /* CMPGestureRecognizer.m */; };
@@ -87,6 +91,11 @@
8791
997DFCFA2B18E5D3000B56B5 /* CMPUIKitUtilsTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CMPUIKitUtilsTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
8892
997DFCFC2B18E5D3000B56B5 /* CMPUIKitUtilsTestApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMPUIKitUtilsTestApp.swift; sourceTree = "<group>"; };
8993
99BE84D22C3467B100E43826 /* CMPUIKitUtilsTestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CMPUIKitUtilsTestApp.xctestplan; sourceTree = "<group>"; };
94+
99CC4B282ECE04AC007C5C44 /* CMPComposeContainerLifecycleDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CMPComposeContainerLifecycleDelegate.h; sourceTree = "<group>"; };
95+
99CC4B2B2ECE07EA007C5C44 /* CMPComposeContainerLifecycleState.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CMPComposeContainerLifecycleState.h; sourceTree = "<group>"; };
96+
99CC4B2C2ECE0838007C5C44 /* CMPView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CMPView.h; sourceTree = "<group>"; };
97+
99CC4B2D2ECE0838007C5C44 /* CMPView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CMPView.m; sourceTree = "<group>"; };
98+
99CC4B312ECE16C8007C5C44 /* CMPViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMPViewTests.swift; sourceTree = "<group>"; };
9099
99D97A862BF73A9B0035552B /* CMPEditMenuView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CMPEditMenuView.h; sourceTree = "<group>"; };
91100
99D97A872BF73A9B0035552B /* CMPEditMenuView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CMPEditMenuView.m; sourceTree = "<group>"; };
92101
99DCAB0C2BD00F5C002E6AC7 /* CMPTextLoupeSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CMPTextLoupeSession.h; sourceTree = "<group>"; };
@@ -140,6 +149,8 @@
140149
996EFEEB2B02CE5D0000FE0F /* CMPUIKitUtils */ = {
141150
isa = PBXGroup;
142151
children = (
152+
99CC4B282ECE04AC007C5C44 /* CMPComposeContainerLifecycleDelegate.h */,
153+
99CC4B2B2ECE07EA007C5C44 /* CMPComposeContainerLifecycleState.h */,
143154
EA70A7E62B27106100300068 /* CMPAccessibilityElement.h */,
144155
EA70A7E82B27106100300068 /* CMPAccessibilityElement.m */,
145156
EADD028E2C9846D9003F66E8 /* CMPDragInteractionProxy.h */,
@@ -174,6 +185,8 @@
174185
996EFEF52B02CE8A0000FE0F /* CMPUIKitUtils.h */,
175186
997DFCDC2B18D135000B56B5 /* CMPViewController.h */,
176187
997DFCDD2B18D135000B56B5 /* CMPViewController.m */,
188+
99CC4B2C2ECE0838007C5C44 /* CMPView.h */,
189+
99CC4B2D2ECE0838007C5C44 /* CMPView.m */,
177190
);
178191
path = CMPUIKitUtils;
179192
sourceTree = "<group>";
@@ -204,6 +217,7 @@
204217
children = (
205218
997DFCF02B18DBF2000B56B5 /* CMPUIKitUtilsTests-Bridging-Header.h */,
206219
997DFCE52B18D99E000B56B5 /* CMPViewControllerTests.swift */,
220+
99CC4B312ECE16C8007C5C44 /* CMPViewTests.swift */,
207221
997DFCF12B18DE47000B56B5 /* Utils */,
208222
);
209223
path = CMPUIKitUtilsTests;
@@ -354,6 +368,7 @@
354368
9968C35B2D76FE16005E8DE4 /* CMPPanGestureRecognizer.m in Sources */,
355369
991A97F72E1FB99300B47130 /* CMPScrollView.m in Sources */,
356370
99D97A882BF73A9B0035552B /* CMPEditMenuView.m in Sources */,
371+
99CC4B2E2ECE0838007C5C44 /* CMPView.m in Sources */,
357372
EABD912B2BC02B5F00455279 /* CMPInteropWrappingView.m in Sources */,
358373
EADD02902C9846D9003F66E8 /* CMPDragInteractionProxy.m in Sources */,
359374
992EDDFB2E55EC8400FB44C5 /* CMPKeyValueObserver.m in Sources */,
@@ -371,6 +386,8 @@
371386
isa = PBXSourcesBuildPhase;
372387
buildActionMask = 2147483647;
373388
files = (
389+
99CC4B2F2ECE0838007C5C44 /* CMPView.m in Sources */,
390+
99CC4B322ECE16C8007C5C44 /* CMPViewTests.swift in Sources */,
374391
997DFCF52B18E276000B56B5 /* XCTestCase.swift in Sources */,
375392
997DFCE62B18D99E000B56B5 /* CMPViewControllerTests.swift in Sources */,
376393
997DFCEE2B18DB7B000B56B5 /* CMPViewController.m in Sources */,
@@ -387,6 +404,7 @@
387404
EAC703E52B8C826E001ECDA6 /* CMPOSLogger.m in Sources */,
388405
997DFCFD2B18E5D3000B56B5 /* CMPUIKitUtilsTestApp.swift in Sources */,
389406
EAC703E62B8C826E001ECDA6 /* CMPOSLoggerInterval.m in Sources */,
407+
99CC4B302ECE0838007C5C44 /* CMPView.m in Sources */,
390408
);
391409
runOnlyForDeploymentPostprocessing = 0;
392410
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <UIKit/UIKit.h>
18+
19+
@protocol CMPComposeContainerLifecycleDelegate
20+
21+
- (void)composeContainerWillAppear;
22+
- (void)composeContainerDidDisappear;
23+
- (void)composeContainerWillDealloc;
24+
25+
@end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <UIKit/UIKit.h>
18+
19+
typedef NS_ENUM(NSInteger, CMPComposeContainerLifecycleState) {
20+
CMPComposeContainerLifecycleStateInitialized,
21+
CMPComposeContainerLifecycleStateStarted,
22+
CMPComposeContainerLifecycleStateStopped
23+
};

compose/ui/ui-uikit/src/uikitMain/objc/CMPUIKitUtils/CMPUIKitUtils/CMPUIKitUtils.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ FOUNDATION_EXPORT double CMPUIKitUtilsVersionNumber;
2323
FOUNDATION_EXPORT const unsigned char CMPUIKitUtilsVersionString[];
2424

2525
#import "CMPViewController.h"
26+
#import "CMPView.h"
2627
#import "CMPAccessibilityElement.h"
2728
#import "CMPOSLogger.h"
2829
#import "CMPTextLoupeSession.h"
@@ -34,3 +35,4 @@ FOUNDATION_EXPORT const unsigned char CMPUIKitUtilsVersionString[];
3435
#import "CMPHoverGestureHandler.h"
3536
#import "CMPScreenEdgePanGestureRecognizer.h"
3637
#import "CMPScrollView.h"
38+
#import "CMPComposeContainerLifecycleDelegate.h"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <UIKit/UIKit.h>
18+
#import "CMPComposeContainerLifecycleDelegate.h"
19+
20+
NS_ASSUME_NONNULL_BEGIN
21+
22+
@interface CMPView : UIView
23+
24+
- (id)initWithLifecycleDelegate:(id<CMPComposeContainerLifecycleDelegate> _Nullable)delegate;
25+
26+
/// Notifies the view is added to a view hierarchy
27+
- (void)viewDidAppear;
28+
29+
/// Notifies the view was removed from a view hierarchy
30+
- (void)viewDidDisappear;
31+
32+
/// Indicates that view is considered alive in terms of structural containment
33+
- (void)viewDidEnterWindowHierarchy;
34+
35+
/// Indicates that view is considered as closed in terms of structural containment
36+
- (void)viewDidLeaveWindowHierarchy;
37+
38+
/// Indicates that trait interface style trait changed
39+
- (void)userInterfaceStyleDidChange;
40+
41+
@end
42+
43+
NS_ASSUME_NONNULL_END
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import "CMPView.h"
18+
#import "CMPComposeContainerLifecycleState.h"
19+
20+
#pragma mark - CMPViewController
21+
22+
@implementation CMPView {
23+
CMPComposeContainerLifecycleState _lifecycleState;
24+
id<CMPComposeContainerLifecycleDelegate> _lifecycleDelegate;
25+
BOOL _isViewInWindowHierarchy;
26+
}
27+
28+
- (id)initWithLifecycleDelegate:(id<CMPComposeContainerLifecycleDelegate>)delegate {
29+
self = [super initWithFrame:CGRectZero];
30+
31+
if (self) {
32+
_lifecycleDelegate = delegate;
33+
_lifecycleState = CMPComposeContainerLifecycleStateInitialized;
34+
_isViewInWindowHierarchy = NO;
35+
36+
__weak typeof(self) weakSelf = self;
37+
if (@available(iOS 17, *)) {
38+
[self registerForTraitChanges:@[[UITraitUserInterfaceStyle class]]
39+
withHandler:^(__kindof id<UITraitEnvironment> _Nonnull traitEnvironment,
40+
UITraitCollection * _Nonnull previousCollection) {
41+
[weakSelf userInterfaceStyleDidChange];
42+
}];
43+
}
44+
}
45+
46+
return self;
47+
}
48+
49+
- (void)didMoveToWindow {
50+
[super didMoveToWindow];
51+
52+
[self updateViewAppearanceState];
53+
}
54+
55+
- (void)didMoveToSuperview {
56+
[super didMoveToSuperview];
57+
58+
[self updateViewAppearanceState];
59+
}
60+
61+
- (void)updateViewAppearanceState {
62+
BOOL isViewInWindowHierarchy = self.superview != nil && self.window != nil;
63+
if (_isViewInWindowHierarchy != isViewInWindowHierarchy) {
64+
_isViewInWindowHierarchy = isViewInWindowHierarchy;
65+
if (isViewInWindowHierarchy) {
66+
[_lifecycleDelegate composeContainerWillAppear];
67+
[self transitViewLifecycleToStarted];
68+
[self viewDidAppear];
69+
} else {
70+
[self viewDidDisappear];
71+
[self scheduleViewHierarchyContainmentCheck];
72+
[_lifecycleDelegate composeContainerDidDisappear];
73+
}
74+
}
75+
}
76+
77+
- (void)transitViewLifecycleToStarted {
78+
switch (_lifecycleState) {
79+
case CMPComposeContainerLifecycleStateInitialized:
80+
case CMPComposeContainerLifecycleStateStopped:
81+
_lifecycleState = CMPComposeContainerLifecycleStateStarted;
82+
[self viewDidEnterWindowHierarchy];
83+
break;
84+
case CMPComposeContainerLifecycleStateStarted:
85+
break;
86+
}
87+
}
88+
89+
- (void)scheduleViewHierarchyContainmentCheck {
90+
double delayInSeconds = 0.5;
91+
92+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
93+
switch (self->_lifecycleState) {
94+
case CMPComposeContainerLifecycleStateInitialized:
95+
NSAssert(false, @"Attempt to schedule hierarchy check without starting the container");
96+
break;
97+
case CMPComposeContainerLifecycleStateStopped:
98+
break;
99+
case CMPComposeContainerLifecycleStateStarted:
100+
// perform check
101+
if (!self->_isViewInWindowHierarchy) {
102+
self->_lifecycleState = CMPComposeContainerLifecycleStateStopped;
103+
[self viewDidLeaveWindowHierarchy];
104+
}
105+
break;
106+
}
107+
});
108+
}
109+
110+
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
111+
[super traitCollectionDidChange:previousTraitCollection];
112+
113+
if (@available(iOS 17, *)) {
114+
// Do nothing
115+
} else if (self.traitCollection.userInterfaceStyle != previousTraitCollection.userInterfaceStyle) {
116+
[self userInterfaceStyleDidChange];
117+
}
118+
}
119+
120+
- (void)viewDidAppear {
121+
}
122+
123+
- (void)viewDidDisappear {
124+
}
125+
126+
- (void)viewDidEnterWindowHierarchy {
127+
}
128+
129+
- (void)viewDidLeaveWindowHierarchy {
130+
}
131+
132+
- (void)userInterfaceStyleDidChange {
133+
}
134+
135+
- (void)dealloc {
136+
if (_lifecycleState == CMPComposeContainerLifecycleStateStarted) {
137+
[self viewDidLeaveWindowHierarchy];
138+
}
139+
140+
[_lifecycleDelegate composeContainerWillDealloc];
141+
}
142+
143+
@end

compose/ui/ui-uikit/src/uikitMain/objc/CMPUIKitUtils/CMPUIKitUtils/CMPViewController.h

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,22 @@
1515
*/
1616

1717
#import <UIKit/UIKit.h>
18+
#import "CMPComposeContainerLifecycleDelegate.h"
1819

1920
NS_ASSUME_NONNULL_BEGIN
2021

21-
@protocol CMPViewControllerLifecycleDelegate
22-
23-
- (void)viewControllerWillAppear;
24-
- (void)viewControllerDidDisappear;
25-
- (void)viewControllerWillDealloc;
26-
27-
@end
28-
2922
@interface CMPViewController : UIViewController
3023

31-
- (id)initWithLifecycleDelegate:(id<CMPViewControllerLifecycleDelegate> _Nullable)delegate;
24+
- (id)initWithLifecycleDelegate:(id<CMPComposeContainerLifecycleDelegate> _Nullable)delegate;
3225

33-
/// Indicates that view controller is considered alive in terms of structural containment.
34-
/// Overriding classes should call super.
26+
/// Indicates that view controller is considered alive in terms of structural containment
3527
- (void)viewControllerDidEnterWindowHierarchy;
3628

37-
/// Indicates that view controller is considered alive in terms of structural containment
38-
/// Overriding classes should call super.
29+
/// Indicates that view controller is considered closed in terms of structural containment
3930
- (void)viewControllerDidLeaveWindowHierarchy;
4031

41-
42-
// MARK: Unexported methods redeclaration block
43-
// Redeclared to make it visible to Kotlin for override purposes, workaround for the following issue:
44-
// https://youtrack.jetbrains.com/issue/KT-56001/Kotlin-Native-import-Objective-C-category-members-as-class-members-if-the-category-is-located-in-the-same-file
45-
46-
- (void)viewSafeAreaInsetsDidChange;
32+
/// Indicates that trait interface style trait changed
33+
- (void)userInterfaceStyleDidChange;
4734

4835
@end
4936

0 commit comments

Comments
 (0)