How do you get the current scene's status bar height for use in a SwiftUI view?
It looks like the key is accessing the current scene. From a view controller, you can use view.window?.windowScene to get to the scene, but what about in a SwiftUI view?
Store the window scene in an environment object when creating the view hierarchy. The environment object can be an existing object that you use for other app-wide data.
final class AppData: ObservableObject {
let windowScene: UIWindow? // Will be nil in SwiftUI previewers
init(windowScene: UIWindowScene? = nil) {
self.windowScene = windowScene
}
}
Set the window scene when you create the environment object. Add the object to the view at the base of your view hierarchy, such as the root view.
let rootView = RootView().environmentObject(AppData(windowScene: windowScene))
Finally, use the window scene to access the status bar height.
struct MyView: View {
#EnvironmentObject private var appData: AppData
...
var body: some View {
SomeView().frame(height: self.appData.windowScene?.statusBarManager?.statusBarFrame.height ?? 0)
}
}
Related
I have a NSStatusBarButton that contains a NSHostingView containing a Swift UI View.
I'd like that my Button and his subview to be the size of the Swift UI view content size.
The code looks like that:
// CustomSwiftUIView
import SwiftUI
struct CustomSwiftUIView: View {
var body: some View {
Text("Hello world, I have a dynamic width :)")
}
}
// AppDelegate
class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let button = statusItem.button {
let mySwiftUIView = CustomSwiftUIView()
let innerView = NSHostingView(rootView: mySwiftUIView);
button.addSubview(innerView)
}
}
}
If I don't explicitly set a width and an height to the Button AND to the innerView, the button size is 0,0 and nothing is displayed.
Any idea how I can achieve the desired behavior?
Thanks!
In SwiftUI, I am trying to create some binding between a parent ViewModel and a child ViewModel, here is a simplified example of my scenario:
The parent component:
class ParentViewModel : ObservableObject {
#Published var name = "John Doe"
func updateName() {
self.name = "Jonnie Deer"
}
}
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
ChildView(name: $viewModel.name)
// tapping the button the text on parent view is updated but not in child view
Button("Update", action: viewModel.updateName)
}
}
}
The child component:
class ChildViewModel : ObservableObject {
var name: Binding<String>
var displayName: String {
get {
return "Hello " + name.wrappedValue
}
}
init(name: Binding<String>) {
self.name = name
}
}
struct ChildView: View {
#StateObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
init(name: Binding<String>) {
_viewModel = StateObject(wrappedValue: ChildViewModel(name: name))
}
}
So, as stated in the comments, when I tap the button on the parent component the name is not getting updated in ChildView, as if the binding is lost...
Is there any other way to update view model with the updated value? say something like getDerivedStateFromProps in React (becuase when tapping the button the ChildView::init method is called with the new name.
Thanks.
Apple is very big on the concept of a Single Source of Truth(SSoT), and keeping it in mind will keep you from getting into the weeds in code like this. The problem you are having is that while you are using a Binding to instantiate the child view, you are turning around and using it as a #StateObject. When you do that, you are breaking the connection as #StateObject is supposed to sit at the top of the SSoT hierarchy. It designates your SSoT. Otherwise, you have two SSoTs, so you can only update one. The view model in ChildView should be an #ObservedObject so that it connects back up the hierarchy. Also, you can directly instantiate the ChildViewModel when you call ChildView. The initializer just serves to decouple things. Your views would look like this:
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
// You can directly use the ChildViewModel to instantiate the ChildView
ChildView(viewModel: ChildViewModel(name: $viewModel.name))
Button("Update", action: viewModel.updateName)
}
}
}
struct ChildView: View {
// Make this an #ObservedObject not a #StateObject
#ObservedObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
}
Neither view model is changed.
Get rid of the view model objects and do #State var name = “John” in ParentView and #Binding var name: String in ChildView. And pass $name into ChildView’s init which gives you write access as if ParentView was a view model object.
By using #State and #Binding you get the reference type semantics you want inside a value type which is the power of SwiftUI. If you just use objects you lose that benefit and have more work to do.
We usually only use ObservableObject for model data but we can also use it for loaders/fetchers where we want to tie some controller behaviour to the view lifecycle but for data transient to a view we always use #State and #Binding. You can extract related vars into their own struct and use mutating funcs for other logic and thus have a single #State struct used by body instead of multiple. This way it can still be testable like a view model object in UIKit would be.
To change TabBar background and other properties you have about 2 ways to process.
via the init() of the view containing the TabView
(see: https://stackoverflow.com/a/56971573/2192483)
via an .onAppear() directly on the TabView
(see: https://stackoverflow.com/a/63414605/2192483 - thanks to #Asperi ;
see: https://schwiftyui.com/swiftui/customizing-your-tabviews-bar-in-swiftui/ - thanks to #Dan O'Leary)
These solutions are efficients at View load and if you reload Tab Bar by touching tabs.
But IT DOES'NT WORK, if you want to change a TabBar properties programmatically, without User interaction on the TabBar Buttons, through the regular way of properties defined in #Published values. This doesn't work because either though init() or .onAppear(), these 2 methods are not subscribers of prop. publishers so the view embedding the TabView doesn't reload.
I finally found a way to trigger the TabView reload not from user tab button touches, with regular #Published properties.
Here is example based on an app with a settings panel allowing to change app skin, including the TabBar background color.
1) The Main struct embed the ContentView that embed the TabView
• SettingsModel embed the properties that share a) the skin values b) a flag to trigger the reload
• ContentView owns one param: skinChanged
#main
struct MyApp: App {
#ObservedObject private var settings = SettingsModel.shared
var body: some Scene {
WindowGroup {
ContentView(skinChanged: settings.skinChanged)
}
}
}
.
2) The ContentView struct embed the TabView
• init(skinChanged: Bool) allows to custom TabBar appearance that
will reload if settings.skinChanged changes of value
• init / if !skinChanged { } applies TabBar appearance only if needed
• body / if !self.settings.skinChanged { } allows to create a change in
the view alternating from TabView to a Rectangle (as a background)
struct ContentView: View {
#ObservedObject private var settings = SettingsModel.shared
init(skinChanged: Bool) {
if !skinChanged {
let tabBarAppearance = UITabBar.appearance()
tabBarAppearance.backgroundColor = UIColor(settings.skin.tabBarBg)
tabBarAppearance.unselectedItemTintColor = UIColor(settings.skin.tabBarUnselected)
}
}
var body: some View {
if !self.settings.skinChanged {
TabView(selection: $someStateProp) {
...
}
} else {
Rectangle()
.fill(settings.skin.tabBarBg)
}
}
}
.
3) The SettingModel struct publishes properties that trigger skin changes and skin values
• In order to create a change in ContentView(skinChanged:) with
settings.skinChanged, the value must pass from false to true then
back to false
• To let the UI applies each changes the 2 changes must be
parallelized with 2 synchronized tasks
class SettingsModel: ObservableObject {
static public let shared = SettingsModel()
...
#Published var skin = Skin(style: .modern)
#Published var skinId = 0 {
didSet {
...
self.skin = Skin(style: Skin.Style.allCases[self.skinId])
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.skinChanged = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.skinChanged = false
}
}
}
#Published var skinChanged = false
...
}
I have this rather common situation where a List is displaying some items from an #ObservedObject data store as NavigationLinks.
On selecting a NavigationLink a DetailView is presented. This view has a simple Toggle conected to a #Published var on its ViewModel class.
When the DetailView appears (onAppear:) its View Model sets the published var that controls the Toggle to true and also triggers an async request that will update the main data store, causing the List in the previous screen to update too.
The problem is that when this happens (the List is reloaded from an action triggered in the Detail View) multiple instances of the DetailViewModel seem to retained and the DetailView starts receiving events from the wrong Publishers.
In the first time the Detail screen is reached the behaviour is correct (as shown in the code below), the toggle is set to true and the store is updated, however, on navigating back to the List and then again to another DetailView the toggle is set to true on appearing but this time when the code that reloads the store executes, the toggle is set back to false.
My understanding is that when the List is reloaded and a new DetailView and ViewModel are created (as the destination of the NavigationLink) the initial value from the isOn variable that controls the Toggle (which is false) is somehow triggering an update to the Toggle of the currently displayed screen.
Am I missing something here?
import SwiftUI
import Combine
class Store: ObservableObject {
static var shared: Store = .init()
#Published var items = ["one", "two"]
private init() { }
}
struct ContentView: View {
#ObservedObject var store = Store.shared
var body: some View {
NavigationView {
List(store.items, id: \.self) { item in
NavigationLink(item, destination: ItemDetailView())
}
}
}
}
struct ItemDetailView: View {
#ObservedObject var viewModel = ItemDetailViewModel()
var body: some View {
VStack {
Toggle("A toggle", isOn: $viewModel.isOn)
Text(viewModel.title)
Spacer()
} .onAppear(perform: viewModel.onAppear)
}
}
class ItemDetailViewModel: ObservableObject {
#ObservedObject var store: Store = .shared
#Published var isOn = false
var title: String {
store.items[0]
}
func onAppear() {
isOn = true
asyncOperationThatUpdatesTheStoreData()
}
private func asyncOperationThatUpdatesTheStoreData() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
self?.store.items = ["three", "four"]
}
}
}
You're controlling lifetimes and objects in a way that's not a pattern in this UI framework. The VieModel is not going to magically republish a singleton value type; it's going to instantiate with that value and then mutate its state without ever checking in with the shared instance again, unless it is rebuilt.
class Store: ObservableObject {
static var shared: Store = .init()
struct ContentView: View {
#ObservedObject var store = Store.shared
struct ItemDetailView: View {
#ObservedObject var viewModel = ItemDetailViewModel()
class ViewModel: ObservableObject {
#Published var items: [String] = Store.shared.items
There are lots of potential viable patterns. For example:
Create class RootStore: ObservableObject and house it as an #StateObject in App. The lifetime of a StateObject is the lifetime of that view hierarchy's lifetime. You can expose it (a) directly to child views by .environmentObject(store) or (b) create a container object, such as Factory that vends ViewModels and pass that via the environment without exposing the store to views, or (c) do both a and b.
If you reference the store in another class, hold a weak reference weak var store: Store?. If you keep state in that RootStore on #Published, then you can subscribe directly to that publisher or to the RootStore's own ObjectWillChangePublisher. You could also use other Combine publishers like CurrentValueSubject to achieve the same as #Published.
For code examples, see
Setting a StateObject value from child view causes NavigationView to pop all views
SwiftUI #Published Property Being Updated In DetailView
I am creating a sign in page for my app and would like to present the home screen in a way that the user can not go back. In Swift UI how do I present it so the new view does not present in a card like style? I know this presenting style is now default for iOS 13.
This is what I already have.
import SwiftUI
struct Test : View {
var body: some View {
PresentationButton(Text("Click to show"), destination: Extra() )
}
}
I would like the view to present full screen.
Use a NavigationView with a NavigationButton and hide the destination view's navigation bar's back button.
For example:
struct ContentView : View {
let destinationView = Text("Destination")
.navigationBarItem(title: Text("Destination View"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Tap Here")
}
}
}
}
You can also disable the destination view's navigation bar altogether by doing let destinationView = Text("Destination").navigationBarHidden(true).