Apple announced SwiftUI as a way to write apps on all devices. But the screensize and UX design principles are varying widely. So the question is how to reuse the same views on all platforms. How to do reactive layout?
This is an advanced discussion. If you are not yet familiar with the basic SwiftUI layout methods such as VStack, HStack and frames, you can check out my other beginner tutorial here.
I have published my app on iPhone and iPad so fare and I am currently working on the mac version. Here I want to share some useful tools. Going from the small iPhone 8 to iPad 12-inch. You can do this device specific, but don’t forget that users will expect split-screen and multi -window support on the ipad. What should happen in portrait and landscape mode?
This is showing one of the main views of my app for iphone and ipad. The user can choose documents. There is a search field and filters. As you can see, for the small screensize parts of the ui are stacked. For the iPad the two main card views are stacked vertical or horizontal depending on the device orientation.
So how do we know how much space we have. One of the most used options I have seen is GeometryReader.
A detail article can be found at swiftui-lab (https://swiftui-lab.com/geometryreader-to-the-rescue/). GeometryReader will take all the space avaliable. So if you want to use the intrinizic size of the subviews, things get more complicated. Another problem is that it will not get the window size unless you put it around all views. Not a nice code to use. Recently, I have experienced an error, that will crash the app. When I try to access geometry.size.width, it tells me it is out of index. Yet another SwfitUI bug, that needs fixing. I cannot find love for GeometryReader 😶.
Environment variables SizeClass
Apple provides a environment var to react to the SizeClass. This is either compact or regular. Down below is a small graphic that gives an overview of the different use cases. It is been aroung a while now. The adaption in SwiftUI is quite straight forward. I guess you can find a way to use it, but this was not giving me the control I wanted. For example all iPhones will be treated the same even though you would not want the same layout on an iPhone SE and iPhone 11. Give me the size of the space!?
SceneDelegate to the Rescue
All the layouts depend on the window size. So where to get that. Investigating the SceneDelegate, I found that there is a call to windowScene didUpdate:
This gets called every time the windowScene bounds and/or the device orientation changes. Every window you open gets an instance of the sceneDelegate and will have this function called seperatrly. This works perfectly fine in multi window, too.
So how do we pass this to our views? Wouldn’t it be nice to have an environment var with all the information, so we can use it wherever we need it? Let us define a class like so:
The appDelegate we adapt like so:
So how do we use this niffty little class? First use case will be to hide a view if the window is too small. It is up to you to choose the maximum window width for showing details. In the example, I chose 400 pixel, because the iPhone8 screen width is 375 and thus to smaller and the iPhone 11 the screen width is larger than 400. I like pixel precision.
DeviceStateManage and Preview Provider
Try running this code on the simulator or device. It is not yet working in the preview. We need to do some extra work there to set the vars, since they stay at their initial zero values. Add this static function to your WindowSizeManager:
This gives us an instance of the WindowSizeManager with the right windowSceneBounds and landscape mode. The code is a bit long, but it should save you some time later. Now we can use the preview provider like so:
In my app, I showed you that I stack views horizontally or vertically depending on the window size. For this we will use a helper struct:
In the body of the ConditionalStack, you can see that we use either a HStack or a VStack. So Bringing this together with the WindowSizeManager:
The conditionalStack uses a variable condition and passes the spacing and alignment modes, so it is fully customisable. Good job us 😄🥳🎉.
How to go from here
If you know Combine, you can define vars in your WindowSizeManage. This is helping to avoid long code and keeps your layout more consistant. Add this to your class:
I let you investigate what isSimilarToPhone does. You don’t need to understand a lot of Combine here . The important thing is that whenever windowSceneBounds change the var isSimilarToPhone is updated. I learned Combine with this book from raywenderlich.com. It uses UIKit only and I hope they add SwiftUI with Combine (using that is like a superpower).
Don’t forget to update the static func default(). Otherwise your previews won’t work with your custom vars.
You can also define your custom stacks by a fixed condition like the following. I even used the UserInterfaceSizeClass. Amazing!
You can use a ZStack on the iPhone and use a toggle to switch between two views, like I did for my iPhone version (see top). Don’t forget you have the option of ScrollViews.
I will write another blog post on layout in macOS. This is even more important since the windowSize is fully adjustable. But we will have to get the windowSize again.
A word of warning
I have to be honesed and tell you about a bug that I was struggling for months. As it turns out it is not a bug. To see what I mean just have a look at this code:
When you run this in the simulator, go to the detail and rotate the device, what happens? Oh no, your taken back to the MainView 🧐. SwiftUI is doing it correctly. When you rotate the device, the view is updated. Since you are changing the VStack to a HStack, the stack is drawn again and with it the NavigationLink inside. So the link is gone. Here is how to avoid this: no more NavigationLinks in adaptive views. We use a state var to set the NavigationLink.
All in all I have to say SwiftUI is amazing for reactive design. Once you know how to change the layout depending on the window size. Hope you will built some cool project with this. Happy Coding!