SwiftUI Repaint View Components on Device Rotation

后端 未结 12 1869
谎友^
谎友^ 2020-12-01 01:00

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

相关标签:
12条回答
  • 2020-12-01 01:41

    Here is an abstraction that allows you to wrap any part of your view tree in optional orientation based behavior, as a bonus, it doesn't rely on UIDevice orientation but instead bases it on the geometry of the space, this allows it to work in swift preview, as well as provide logic for different layouts based specifically on the container for your view:

    struct OrientationView<L: View, P: View> : View {
        let landscape : L
        let portrait : P
    
        var body: some View {
            GeometryReader { geometry in
                Group {
                    if geometry.size.width > geometry.size.height { self.landscape }
                    else { self.portrait }
                }.frame(maxWidth: .infinity, maxHeight: .infinity)
            }
        }
    
        init(landscape: L, portrait: P) {
            self.landscape = landscape
            self.portrait = portrait
        }
    }
    
    struct OrientationView_Previews: PreviewProvider {
        static var previews: some View {
            OrientationView(landscape: Text("Landscape"), portrait: Text("Portrait"))
                .frame(width: 700, height: 600)
                .background(Color.gray)
        }
    }
    

    Usage: OrientationView(landscape: Text("Landscape"), portrait: Text("Portrait"))

    0 讨论(0)
  • 2020-12-01 01:49

    I think easy repainting is possible with addition of

    @Environment(\.verticalSizeClass) var sizeClass
    

    to View struct.

    I have such example:

    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)
    
            }
    
        }
    }
    

    As you can see I need to recalculate tabBarHeight to apply correct bottom padding on Extra View, and addition of this property seems to correctly trigger repainting.

    With just one line of code!

    0 讨论(0)
  • 2020-12-01 01:49

    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)
       }
    }
    
    0 讨论(0)
  • 2020-12-01 01:51

    Inspired by @caram solution, I grab the isLandscape property from windowScene

    In SceneDelegate.swift, get the current orientation from window.windowScene.interfaceOrientation

    ...
    var model = Model()
    ...
    
    func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {    
        model.isLandScape = windowScene.interfaceOrientation.isLandscape
    }
    

    In this way, we'll get true from the start if the user launches the app from the landscape mode.

    Here is the Model

    class Model: ObservableObject {
        @Published var isLandScape: Bool = false
    }
    

    And we can use it in the exact same way as @kontiki suggested

    struct ContentView: View {
        @EnvironmentObject var model: Model
    
        var body: some View {
            Group {
                if model.isLandscape {
                    Text("LANDSCAPE")
                } else {
                    Text("PORTRAIT")
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-01 01:51

    It's easy to go without notifications, delegation methods, events, changes to SceneDelegate.swift, window.windowScene.interfaceOrientation and so on. try running this in simulator and rotating device.

    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)
        }
    }
    
    0 讨论(0)
  • 2020-12-01 01:51

    This seems to work for me. Then just init and use Orientation instance as environmentobject

    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)
            }
        }
    
    0 讨论(0)
提交回复
热议问题