How to detect device rotation in SwiftUI and re-draw view components?
I have a @State variable initialized to the value of UIScreen.main.bounds.width when the first
I wanted to know if there is simple solution within SwiftUI that works with any enclosed view so it can determine a different landscape/portrait layout. As briefly mentioned by @dfd GeometryReader can be used to trigger an update.
Note that this works in the special occasions where use of the standard size class/traits do not provide sufficient information to implement a design. For example, where a different layout is required for portrait and landscape but where both orientations result in a standard size class being returned from the environment. This happens with the largest devices, like the max sized phones and with iPads.
This is the 'naive' version and this does not work.
struct RotatingWrapper: View {
var body: some View {
GeometryReader { geometry in
if geometry.size.width > geometry.size.height {
LandscapeView()
}
else {
PortraitView()
}
}
}
}
This following version is a variation on a rotatable class that is a good example of function builders from @reuschj but just simplified for my application requirements https://github.com/reuschj/RotatableStack/blob/master/Sources/RotatableStack/RotatableStack.swift
This does work
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))
}
}
}
}
That is interesting because I'm assuming that some SwiftUI magic has caused this apparently simple semantic change to activate the view re-rendering.
One more weird trick that you can use this for, is to 'hack' a re-render this way, throw away the result of using the GeometryProxy and perform a Device orientation lookup. This then enables use of the full range of orientations, in this example the detail is ignored and the result used to trigger a simple portrait and landscape selection or whatever else is required.
enum Orientation {
case landscape
case portrait
}
struct RotatingWrapper: View {
func getOrientation(geometry:GeometryProxy) -> Orientation {
let _ = geometry.size.width > geometry.size.height
if UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft || UIDevice.current.orientation == UIDeviceOrientation.landscapeRight {
return .landscape
}
else {
return .portrait
}
}
var body: some View {
ZStack {
GeometryReader { geometry in
if self.getOrientation(geometry: geometry) == .landscape {
LandscapeView()
}
else {
PortraitView()
}
}
}
}
}
Furthermore, once your top level view is being refreshed you can then use DeviceOrientation directly, such as the following in child views as all child views will be checked once the top level view is 'invalidated'
Eg: In the LandscapeView() we can format child views appropriately for its horizontal position.
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)
}
}