I'm using SwiftUI with Big Sur and the life cycle of SwiftUI (not AppDelegate):
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
In this case, I can open multiple windows with cmd+n. How can I find out, which window is the one that is focused?
Related
[Xcode 14.1, iOS 16.1]
I have a NavigationStack with a navigationTitle and a TabView with 2 Views. Each View has a ScrollView (see image below):
NavigationStack and TabView problem image
When I tap on Tab1 (#1 in red on the image above), then swipe up, the behavior is as expected (#2), i.e. the big navigationTitle move to the center, and my view passes below and becomes blurry. Perfect.
However, when I tap ton Tab2 (#3) and then swipe up (#4), the big title stays big, and the view doesn't become blurry.
Then I tap on Tab1 again (#5) and it works as expected.
Please help!
Here is my code:
ContentView:
import SwiftUI
struct ContentView: View {
#State private var selection: Tab = .tab1
enum Tab {
case tab1
case tab2
}
#State private var mainTitle = "Tab1"
var body: some View {
NavigationStack {
TabView(selection: $selection) {
Tab1(mainTitle: $mainTitle)
.tabItem {
Label("Tab1", systemImage: "wrench.adjustable.fill")
}
.tag(Tab.tab1)
Tab2(mainTitle: $mainTitle)
.tabItem {
Label("Tab2", systemImage: "wrench.adjustable.fill")
}
.tag(Tab.tab2)
} .navigationTitle(mainTitle)
}
}
}
Tab1:
import SwiftUI
struct Tab1: View {
#Binding var mainTitle : String
var body: some View {
ScrollView {
Text("Text tab 1")
.padding(.all,100)
.background(.blue)
} .onAppear {
mainTitle = "Tab1"
}
}
}
Tab2:
import SwiftUI
struct Tab2: View {
#Binding var mainTitle : String
var body: some View {
ScrollView {
Text("Text tab 2")
.padding(.all,100)
.background(.green)
} .onAppear {
mainTitle = "Tab2"
}
}
}
I tried a hack that is supposed to fix the transparency bug for Tab bars, but it doesn't work.
.onAppear {
let tabBarAppearance = UITabBarAppearance()
tabBarAppearance.configureWithOpaqueBackground()
UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
}
TabViews are designed to sit at the top of the navigation hierarchy. They're intended to allow users to switch between independent sections of your app at any time.
You would generally put a separate navigation stack within each tab that then handles pushing and popping of views. And then, you can use the navigationTitle modifier to manage the screen's title.
So your structure (which might be split over multiple custom views) should look something like:
TabView {
NavigationStack {
ScrollView {
}
.navigationTitle("Tab 1")
}
.tabItem { Label("Tab1", ...) }
NavigationStack {
ScrollView {
}
.navigationTitle("Tab 2")
}
.tabItem { Label("Tab2", ...) }
}
This structure is by design, to align with Apple's Human Interface Guidelines. It's worth reading the HIG to get a handle on where Apple are coming from, and how working on the same principles can really help your app feel like it belongs on your users' device.
On app launch, I want to get the horizontalSizeClass and based on if it's compact or regular, apply a navigation style to my root navigation view like so:
import SwiftUI
#main
struct MyApp: App {
#Environment(\.horizontalSizeClass) var sizeClass
var body: some Scene {
WindowGroup {
if sizeClass == .compact {
NavigationView {
Text("Compact size class inside stack navigation style")
}
.navigationViewStyle(StackNavigationViewStyle())
} else {
NavigationView {
Text("Regular size class inside default navigation style")
}
}
}
}
}
However, sizeClass always returns nil in this case.
How do I
determine if the horizontal size class is compact or regular on the root view, and
make the navigation style adapt to the size class any time it changes
My app is targeting iOS 14 for both iPhone and iPad.
Any help or a different approach to adapt for size class changes for the whole app is much appreciated.
Update 1
I tried the suggestions to use a ViewModifier or creating a custom view and adding the navigation in it's body like so:
import SwiftUI
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
MyRootView()
}
}
}
struct MyRootView: View {
#Environment(\.horizontalSizeClass) var sizeClass
var body: some View {
if sizeClass == .compact {
NavigationView {
Text("Compact size class inside stack navigation style")
}
.navigationViewStyle(StackNavigationViewStyle())
} else {
NavigationView {
Text("Regular size class inside default navigation style")
}
}
}
}
However, the navigation stack pops to the root view every time the sizeClass changes. Is there a way to preserve the stack? For example: If the user is 5 levels deep in navigation, and sizeClass changes, change the navigation style while keeping the visible screen?
Thank you!
Update 2
I was able to find a WWDC session explaining exactly what I want, but it's in UIKit.
See 18:35 here: https://developer.apple.com/wwdc20/10105
I'm trying to achieve the same goal in SwiftUI (keep the screen the user selected while changing the size class to compact).
According to the session, UISplitViewController supports this because there's the concept of Restorable and Restore in the detail view. I can't find a way to do this in SwiftUI.
this setup works for me. I read somewhere in the docs that Environment are updated before a view is rendered.
I guess App is not a view.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#Environment(\.horizontalSizeClass) var horizontalSizeClass
var body: some View {
if horizontalSizeClass == .compact {
Text("Compact")
} else {
Text("Regular")
}
}
}
Yeah, forgot about the NavigationViews.
You could try something like this (using the code from "https://matteo-puccinelli.medium.com/conditionally-apply-modifiers-in-swiftui-51c1cf7f61d1")
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
extension View {
#ViewBuilder
func ifCondition<TrueContent: View, FalseContent: View>(_ condition: Bool, then trueContent: (Self) -> TrueContent, else falseContent: (Self) -> FalseContent) -> some View {
if condition {
trueContent(self)
} else {
falseContent(self)
}
}
}
struct ContentView: View {
#Environment(\.horizontalSizeClass) var sizeClass
var body: some View {
NavigationView {
if sizeClass == .compact {
Text("Compact size class inside stack navigation style")
} else {
Text("Regular size class inside default navigation style")
}
}
.ifCondition(sizeClass == .compact) { nv in
nv.navigationViewStyle(StackNavigationViewStyle())
} else: { nv in
nv.navigationViewStyle(DefaultNavigationViewStyle())
}
}
}
I'm using Introspect to hide the tab bar on child navigation link pages. However, I've noticed some odd behavior when the app is backgrounded and then brought back to the foreground.
It seems like initially, the hidden tab bar is still taking up some space, but this disappears when cycling the app back to the foreground. I'm not sure if this is SwiftUI behavior or has to do with how I'm using Introspect / UIKit.
It's causing layout issues in my app, so I'd like to make the spacing consistent if possible.
Here's a minimal example that shows the behavior:
import SwiftUI
import Introspect
struct ContentView: View {
var body: some View {
TabView {
VStack {
Spacer()
Text("Hello, world!")
}
}
.border(Color.red)
.introspectTabBarController { tabBarController in
tabBarController.tabBar.isHidden = true
}
}
}
Here is the late answer. Basically add tabbar height to current view frame. And onDissappear restore view frame size
import SwiftUI
import Introspect
#State var uiTabarController: UITabBarController?
#State var tabBarFrame: CGRect?
struct ContentView: View {
var body: some View {
TabView {
VStack {
Spacer()
Text("Hello, world!")
}
}
.border(Color.red)
.introspectTabBarController { (UITabBarController) in
uiTabarController = UITabBarController
self.tabBarFrame = uiTabarController?.view.frame
uiTabarController?.tabBar.isHidden = true
uiTabarController?.view.frame = CGRect(x:0, y:0, width:tabBarFrame!.width, height:tabBarFrame!.height+UITabBarController.tabBar.frame.height);
}
.onDisappear {
if let frame = self.tabBarFrame {
self.uiTabarController?.tabBar.isHidden = false
uiTabarController?.view.frame = frame
}
}
}
}
In Xcode 12 template for a SwiftUI project using Core Data, Apple provides the following code:
import SwiftUI
#main
struct CoreDataApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
In this case, the persistenceController is a singleton.
What's the difference with following code, where the singleton is initialized and passed at the same time? Why do we use a constant for the singleton?
import SwiftUI
#main
struct CoreDataApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
}
}
}
Also, are there any advantages to providing this singleton to the environment? Couldn't we just use PersistenceController.shared.container.viewContext directly in subviews?
I'm asking because I have few services singletons in my app but I don't want to initialize them from the main App but in subviews.
I am new to swiftUI and am having difficulty moving from one UIViewController to another UIViewController. Rights now I have a state called navigate and a button, but I am not sure how to move this screen to a new screen. Here is the code.
import SwiftUI
struct ContentView: View {
#State var navigate = false
var body: some View {
Button(action: { self.navigate.toggle() }) {
Text("Get Involved")
.font(.custom("PlayfairDisplay-Regular", size: 18))
.foregroundColor(Color("Dark Text"))
}
}
}
Here is the code for the blank screen.
import SwiftUI
struct GoalIdeasView: View {
var body: some View {
Text("Hello")
}
}
How would I go about navigating to the second screen from the ContentView Controller once the button is pressed?
There is a view called Navigation that act like UINavigationViewController. Then you can use a NavigationLink to navigate between views. So it would be like:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink("Get Involved", destination: GoalIdeasView())
.font(.custom("PlayfairDisplay-Regular", size: 18))
}
}
}