SwiftUI在设备旋转时重新绘制视图组件
如何在SwiftUI中检测设备旋转并重新绘制视图组件SwiftUI在设备旋转时重新绘制视图组件,swiftui,Swiftui,如何在SwiftUI中检测设备旋转并重新绘制视图组件 我有一个@State变量,在第一次出现时初始化为UIScreen.main.bounds.width的值。但当设备方向改变时,该值不会改变。当用户更改设备方向时,我需要重新绘制所有组件。@dfd提供了两个很好的选项,我正在添加第三个选项,这是我使用的选项 在我的例子中,我创建了UIHostingController子类,并在函数viewWillTransition中发布了一个自定义通知 然后,在我的环境模型中,我监听这样的通知,它可以在任何视
我有一个@State变量,在第一次出现时初始化为UIScreen.main.bounds.width的值。但当设备方向改变时,该值不会改变。当用户更改设备方向时,我需要重新绘制所有组件。@dfd提供了两个很好的选项,我正在添加第三个选项,这是我使用的选项 在我的例子中,我创建了UIHostingController子类,并在函数viewWillTransition中发布了一个自定义通知 然后,在我的环境模型中,我监听这样的通知,它可以在任何视图中使用
struct ContentView: View {
@EnvironmentObject var model: Model
var body: some View {
Group {
if model.landscape {
Text("LANDSCAPE")
} else {
Text("PORTRAIT")
}
}
}
}
在SceneDelegate.swift中:
func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {
model.environment.toggle()
}
window.rootViewController=MyUIHostingController(rootView:ContentView().environmentObject(模型(isLandscape:windowScene.interfaceOrientation.isLandscape)))
My UIHostingController子类:
扩展通知。名称{
静态let my_onViewWillTransition=Notification.Name(“MainUIHostingController_viewWillTransition”)
}
类MyUIHostingController:UIHostingController,其中内容:视图{
重写func viewWillTransition(到大小:CGSize,带协调器:UIViewControllerTransitionCoordinator){
NotificationCenter.default.post(名称:.my_onViewWillTransition,对象:nil,用户信息:[“大小”:大小])
super.viewWillTransition(到:大小,带:协调器)
}
}
我的模型是:
类模型:ObservableObject{
@已发布变量:Bool=false
初始化(isLandscape:Bool){
self.scape=isLandscape//初始值
NotificationCenter.default.addObserver(self,选择器:#选择器(onViewWillTransition,通知:)),名称:.my_onViewWillTransition,对象:nil)
}
@objc func onViewWillTransition(通知:通知){
guard let size=notification.userInfo?[“size”]作为?CGSize else{return}
横向=尺寸.宽度>尺寸.高度
}
}
有一个更简单的解决方案,由@kontiki提供,无需通知或与UIKit集成
在SceneDelegate.swift中:
func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {
model.environment.toggle()
}
在Model.swift中:
最终类模型:ObservableObject{
let objectWillChange=observeObjectPublisher()
var环境:Bool=false{willSet{objectWillChange.send()}
}
净效果是,依赖于
@EnvironmentObject model
的视图将在每次环境变化时重新绘制,无论是旋转、大小变化等。如果有人也对初始设备方向感兴趣。我是这样做的:
斯威夫特
导入联合收割机
最终类设备:ObservieObject{
@已发布的var isLandscape:Bool=false
}
斯威夫特美术馆
class SceneDelegate:UIResponder,UIWindowSceneDelegate{
变量窗口:UIWindow?
//创建实例
让device=device()//在此处更改
func场景(场景:UIScene,willConnectTo会话:UISceneSession,选项connectionOptions:UIScene.connectionOptions){
// ...
//在此处添加实例作为环境对象
让contentView=contentView().environment(\.managedObjectContext,context).environmentObject(设备)
如果让windowScene=场景为?UIWindowScene{
//在此处读取初始设备方向
device.isLandscape=(windowScene.interfaceOrientation.isLandscape==true)
// ...
}
}
//增加了此功能以在设备旋转时进行注册
func windowScene(windowScene:UIWindowScene,didUpdate-previousCoordinateSpace:UICoordinateSpace,interfaceOrientation-previousInterfaceOrientation:UIInterfaceOrientation,traitCollection-previousTraitCollection:UITracitCollection){
device.isLandscape.toggle()
}
// ...
}
我认为添加
@Environment(\.verticalSizeClass) var sizeClass
查看结构
我有这样的例子:
struct MainView: View {
@EnvironmentObject var model: HamburgerMenuModel
@Environment(\.verticalSizeClass) var sizeClass
var body: some View {
let tabBarHeight = UITabBarController().tabBar.frame.height
return ZStack {
HamburgerTabView()
HamburgerExtraView()
.padding(.bottom, tabBarHeight)
}
}
}
如您所见,我需要重新计算tabBarHeight,以便在额外视图上应用正确的底部填充,添加此属性似乎可以正确触发重新绘制
只需一行代码我尝试了前面的一些答案,但遇到了一些问题。其中一个解决方案可以在95%的时间内工作,但会时不时地破坏布局。其他解决方案似乎不符合SwiftUI的工作方式。所以我想出了自己的解决办法。您可能会注意到,它结合了前面几个建议的功能
// Device.swift
import Combine
import UIKit
final public class Device: ObservableObject {
@Published public var isLandscape: Bool = false
public init() {}
}
// SceneDelegate.swift
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var device = Device()
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
.environmentObject(device)
if let windowScene = scene as? UIWindowScene {
// standard template generated code
// Yada Yada Yada
let size = windowScene.screen.bounds.size
device.isLandscape = size.width > size.height
}
}
// more standard template generated code
// Yada Yada Yada
func windowScene(_ windowScene: UIWindowScene,
didUpdate previousCoordinateSpace: UICoordinateSpace,
interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation,
traitCollection previousTraitCollection: UITraitCollection) {
let size = windowScene.screen.bounds.size
device.isLandscape = size.width > size.height
}
// the rest of the file
// ContentView.swift
import SwiftUI
struct ContentView: View {
@EnvironmentObject var device : Device
var body: some View {
VStack {
if self.device.isLandscape {
// Do something
} else {
// Do something else
}
}
}
}
我想知道SwiftUI中是否有一个简单的解决方案可以与任何封闭视图一起工作,以便它可以确定不同的横向/纵向布局。正如@dfd GeometryReader简要提到的,可以使用它来触发更新 请注意,在使用标准尺寸等级/特征不能提供足够信息来实施设计的特殊情况下,这一点有效。例如,纵向和横向需要不同的布局,但两个方向都会导致从环境返回标准大小类。这种情况发生在最大的设备上,比如最大尺寸的手机和iPad 这是“幼稚”的版本,不起作用
struct RotatingWrapper: View {
var body: some View {
GeometryReader { geometry in
if geometry.size.width > geometry.size.height {
LandscapeView()
}
else {
PortraitView()
}
}
}
}
下面这个版本是一个可旋转类的变体,它是@reuschj中函数生成器的一个很好的示例,但只是针对我的应用程序需求进行了简化
这确实有效
struct RotatingWrapper: View {
func getIsLandscape(geometry:GeometryProxy) -> Bool {
return geometry.size.width > geometry.size.height
}
var body: some View {
GeometryReader { geometry in
if self.getIsLandscape(geometry:geometry) {
Text("Landscape")
}
else {
Text("Portrait").rotationEffect(Angle(degrees:90))
}
}
}
}
这很有趣,因为我假设一些SwiftUI魔法导致了这个显然很简单的语义变化,从而激活了视图重新渲染
您可以使用它的另一个奇怪技巧是,以这种方式“破解”重新渲染,丢弃使用GeometryProxy的结果并执行设备方向查找。这样就可以使用全方位,在本例中,细节被忽略,结果用于触发简单的纵向和横向选择
struct LandscapeView: View {
var body: some View {
HStack {
Group {
if UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft {
VerticallyCenteredContentView()
}
Image("rubric")
.resizable()
.frame(width:18, height:89)
//.border(Color.yellow)
.padding([UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft ? .trailing : .leading], 16)
}
if UIDevice.current.orientation == UIDeviceOrientation.landscapeRight {
VerticallyCenteredContentView()
}
}.border(Color.pink)
}
}
...
var model = Model()
...
func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {
model.isLandScape = windowScene.interfaceOrientation.isLandscape
}
class Model: ObservableObject {
@Published var isLandScape: Bool = false
}
struct ContentView: View {
@EnvironmentObject var model: Model
var body: some View {
Group {
if model.isLandscape {
Text("LANDSCAPE")
} else {
Text("PORTRAIT")
}
}
}
}
struct ContentView: View {
let cards = ["a", "b", "c", "d", "e"]
@Environment(\.horizontalSizeClass) var horizontalSizeClass
var body: some View {
let arrOfTexts = {
ForEach(cards.indices) { (i) in
Text(self.cards[i])
}
}()
if (horizontalSizeClass == .compact) {
return VStack {
arrOfTexts
}.erase()
} else {
return VStack {
HStack {
arrOfTexts
}
}.erase()
}
}
}
extension View {
func erase() -> AnyView {
return AnyView(self)
}
}
struct ContentView: View {
@State var orientation = UIDevice.current.orientation
let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
.makeConnectable()
.autoconnect()
var body: some View {
Group {
if orientation.isLandscape {
Text("LANDSCAPE")
} else {
Text("PORTRAIT")
}
}.onReceive(orientationChanged) { _ in
self.orientation = UIDevice.current.orientation
}
}
}
class Orientation: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var isLandScape:Bool = false {
willSet {
objectWillChange.send() }
}
var cancellable: Cancellable?
init() {
cancellable = NotificationCenter.default
.publisher(for: UIDevice.orientationDidChangeNotification)
.map() { _ in (UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight)}
.removeDuplicates()
.assign(to: \.isLandScape, on: self)
}
}
// OrientationInfo.swift
final class OrientationInfo: ObservableObject {
@Published var isLandscape = false
}
// SceneDelegate.swift
var orientationInfo = OrientationInfo()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// ...
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(orientationInfo))
// ...
}
func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {
orientationInfo.isLandscape = windowScene.interfaceOrientation.isLandscape
}
// YourView.swift
@EnvironmentObject var orientationInfo: OrientationInfo
var body: some View {
Group {
if orientationInfo.isLandscape {
Text("LANDSCAPE")
} else {
Text("PORTRAIT")
}
}
}
// GlobalStates.swift
import Foundation
import SwiftUI
class GlobalStates: ObservableObject {
@Published var isLandScape: Bool = false
}
// YourAppNameApp.swift
import SwiftUI
@main
struct YourAppNameApp: App {
// GlobalStates() is an ObservableObject class
var globalStates = GlobalStates()
// Device Orientation
let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
.makeConnectable()
.autoconnect()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(globalStates)
.onReceive(orientationChanged) { _ in
// Set the state for current device rotation
if UIDevice.current.orientation.isFlat {
// ignore orientation change
} else {
globalStates.isLandscape = UIDevice.current.orientation.isLandscape
}
}
}
}
// Now globalStates.isLandscape can be used in any view
// ContentView.swift
import SwiftUI
struct ContentView: View {
@EnvironmentObject var globalStates: GlobalStates
var body: some View {
VStack {
if globalStates.isLandscape {
// Do something
} else {
// Do something else
}
}
}
}
import SwiftUI
struct DemoView: View {
@Environment(\.horizontalSizeClass) var hSizeClass
@Environment(\.verticalSizeClass) var vSizeClass
var body: some View {
VStack {
if hSizeClass == .compact && vSizeClass == .regular {
VStack {
Text("Vertical View")
}
} else {
HStack {
Text("Horizontal View")
}
}
}
}
}
struct ContentView: View {
@State private var isPortrait = false
var body: some View {
Text("isPortrait: \(String(isPortrait))")
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
guard let scene = UIApplication.shared.windows.first?.windowScene else { return }
self.isPortrait = scene.interfaceOrientation.isPortrait
}
}
}
extension UIApplication {
var currentScene: UIWindowScene? {
connectedScenes
.first { $0.activationState == .foregroundActive } as? UIWindowScene
}
}
guard let scene = UIApplication.shared.currentScene else { return }
import SwiftUI
import Foundation
struct TopView: View {
var body: some View {
GeometryReader{
geo in
VStack{
if keepSize(geo: geo) {
ChildView()
}
}.frame(width: geo.size.width, height: geo.size.height, alignment: .center)
}.background(Color.red)
}
func keepSize(geo:GeometryProxy) -> Bool {
MyScreen.shared.width = geo.size.width
MyScreen.shared.height = geo.size.height
return true
}
}
class MyScreen:ObservableObject {
static var shared:MyScreen = MyScreen()
@Published var width:CGFloat = 0
@Published var height:CGFloat = 0
}
struct ChildView: View {
// The presence of this line also allows direct access to up-to-date UIScreen.main.bounds.size.width & .height
@StateObject var myScreen:MyScreen = MyScreen.shared
var body: some View {
VStack{
if myScreen.width > myScreen.height {
Text("Paysage")
} else {
Text("Portrait")
}
}
}
}