Skip to content

Commit 3c37fed

Browse files
committed
disable safeAreaRegions in SwiftUIHostingView, add tests
1 parent c84f393 commit 3c37fed

3 files changed

Lines changed: 157 additions & 1 deletion

File tree

ComposeUI/Project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ComposeUI/Sources/ComposeUI/Components/SwiftUIHostingView.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ public class SwiftUIHostingView<ContentView: SwiftUI.View>: NSHostingView<AnyVie
9999
super.init(rootView: Self.makeWrappedRootView(rootView: rootView))
100100

101101
updateCommonSettings()
102+
103+
if #available(macOS 13.3, *) {
104+
safeAreaRegions = []
105+
}
102106
}
103107

104108
@available(*, unavailable)
@@ -189,6 +193,11 @@ private final class SwiftUIHostingViewController<ContentView: SwiftUI.View>: UIH
189193

190194
// SwiftUI view jumps when scrolling to edges, fix it by disabling safe area.
191195
disableSafeArea()
196+
197+
if #available(iOS 16.4, *) {
198+
// SwiftUI view moves up when keyboard is shown, fix it by disabling keyboard avoidance.
199+
safeAreaRegions = []
200+
}
192201
}
193202

194203
override func viewDidLoad() {
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
//
2+
// SwiftUIHostingViewTests.swift
3+
// ComposéUI
4+
//
5+
// Created by Honghao Zhang on 5/9/26.
6+
// Copyright © 2024 Honghao Zhang.
7+
//
8+
// MIT License
9+
//
10+
// Copyright (c) 2024 Honghao Zhang (github.com/honghaoz)
11+
//
12+
// Permission is hereby granted, free of charge, to any person obtaining a copy
13+
// of this software and associated documentation files (the "Software"), to
14+
// deal in the Software without restriction, including without limitation the
15+
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
16+
// sell copies of the Software, and to permit persons to whom the Software is
17+
// furnished to do so, subject to the following conditions:
18+
//
19+
// The above copyright notice and this permission notice shall be included in
20+
// all copies or substantial portions of the Software.
21+
//
22+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
27+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
28+
// IN THE SOFTWARE.
29+
//
30+
31+
#if canImport(AppKit)
32+
import AppKit
33+
#endif
34+
35+
#if canImport(UIKit)
36+
import UIKit
37+
#endif
38+
39+
import ChouTiTest
40+
import SwiftUI
41+
42+
@testable import ComposeUI
43+
44+
final class SwiftUIHostingViewTests: XCTestCase {
45+
46+
// MARK: - SwiftUIHostingView
47+
48+
func test_init_withRootView() {
49+
let view = SwiftUIHostingView(rootView: SwiftUI.Text("Hello, World!"))
50+
expect(view.bounds.size) == .zero
51+
}
52+
53+
func test_layout_appliesBoundsToContent() {
54+
let view = SwiftUIHostingView(rootView: SwiftUI.Color.black)
55+
view.frame = CGRect(x: 0, y: 0, width: 120, height: 80)
56+
57+
#if canImport(UIKit)
58+
view.layoutIfNeeded()
59+
let hostingControllerView = try? view.subviews.first.unwrap()
60+
expect(hostingControllerView?.frame) == CGRect(x: 0, y: 0, width: 120, height: 80)
61+
#endif
62+
63+
#if canImport(AppKit)
64+
expect(view.bounds.size) == CGSize(width: 120, height: 80)
65+
#endif
66+
}
67+
68+
// MARK: - MutableSwiftUIHostingView
69+
70+
func test_mutable_init_emptyContent() {
71+
let view = MutableSwiftUIHostingView()
72+
expect("\(view.content)") == "AnyView(EmptyView())"
73+
}
74+
75+
func test_mutable_setContent() {
76+
let view = MutableSwiftUIHostingView()
77+
expect("\(view.content)".contains("EmptyView")) == true
78+
79+
view.content = AnyView(SwiftUI.Text("Hello, World!"))
80+
expect("\(view.content)".contains("Hello, World")) == true
81+
82+
view.content = AnyView(SwiftUI.Text("Updated"))
83+
expect("\(view.content)".contains("Updated")) == true
84+
expect("\(view.content)".contains("Hello, World")) == false
85+
}
86+
87+
// MARK: - AppKit specific
88+
89+
#if canImport(AppKit)
90+
91+
func test_appKit_isUserInteractionEnabled_defaultTrue() {
92+
let view = SwiftUIHostingView(rootView: SwiftUI.Text("Hello, World!"))
93+
expect(view.isUserInteractionEnabled) == true
94+
}
95+
96+
func test_appKit_becomeFirstResponder_whenInteractionDisabled_returnsFalse() {
97+
let window = TestWindow()
98+
let view = SwiftUIHostingView(rootView: SwiftUI.Text("Hello, World!"))
99+
window.contentView().addSubview(view)
100+
101+
view.isUserInteractionEnabled = false
102+
expect(view.becomeFirstResponder()) == false
103+
}
104+
105+
func test_appKit_becomeFirstResponder_whenInteractionEnabled_delegatesToSuper() {
106+
let window = TestWindow()
107+
let view = SwiftUIHostingView(rootView: SwiftUI.Text("Hello, World!"))
108+
window.contentView().addSubview(view)
109+
110+
// when enabled, the result mirrors the underlying NSHostingView's response
111+
view.isUserInteractionEnabled = true
112+
let plainHostingView = NSHostingView(rootView: SwiftUI.Text("Hello, World!"))
113+
window.contentView().addSubview(plainHostingView)
114+
expect(view.becomeFirstResponder()) == plainHostingView.becomeFirstResponder()
115+
}
116+
117+
#endif
118+
119+
// MARK: - UIKit specific
120+
121+
#if canImport(UIKit)
122+
123+
func test_uiKit_layoutSubviews_resizesHostingControllerView() {
124+
let view = SwiftUIHostingView(rootView: SwiftUI.Color.black)
125+
view.frame = CGRect(x: 0, y: 0, width: 200, height: 150)
126+
view.layoutIfNeeded()
127+
128+
let hostingControllerView = try? view.subviews.first.unwrap()
129+
expect(hostingControllerView?.frame) == CGRect(x: 0, y: 0, width: 200, height: 150)
130+
131+
// bounds change re-applies the frame
132+
view.frame = CGRect(x: 0, y: 0, width: 300, height: 250)
133+
view.layoutIfNeeded()
134+
expect(hostingControllerView?.frame) == CGRect(x: 0, y: 0, width: 300, height: 250)
135+
}
136+
137+
func test_uiKit_hostingControllerView_hasClearBackground() {
138+
let view = SwiftUIHostingView(rootView: SwiftUI.Text("Hello, World!"))
139+
view.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
140+
view.layoutIfNeeded()
141+
142+
let hostingControllerView = try? view.subviews.first.unwrap()
143+
expect(hostingControllerView?.backgroundColor) == .clear
144+
}
145+
146+
#endif
147+
}

0 commit comments

Comments
 (0)