I'm creating a watchOS app and want to choose the starting view to be the home screen's child. A WWDC20 video (time: 13:50) called it "hierarchical navigation" and showed an example, but did not provide any code.
I've been looking around on how to accomplish this but can not find anything. I'm building the app fully in SwiftUI for watchOS 7+.
How can I start at the child of a view?
Just happened to open SO today and found this question.
If I understand correctly (based on the wwdc vid you shared), what you want to do is to open a child view in of your main view.
Let's set up an example based on what I assume you want to do. There is an entry point, which I will name HomeView, hosting your navigation view as well as the navigation links. You will also have two more views (just for illustration purposes), will I'll call AView and BView.
If I create this new app (StraightToDetailsApp), the main entry point should look like:
import SwiftUI
#main
struct StraightToTheDetailsApp: App {
var body: some Scene {
WindowGroup {
Text("Hello world!")
}
}
}
Let's add the three new views we need:
import SwiftUI
#main
struct StraightToTheDetailsApp: App {
var body: some Scene {
WindowGroup {
// replaced ContentView with HomeView
HomeView()
}
}
}
struct HomeView: View {
var body: some Scene {
NavigationView {
ScrollView {
// specified the navigation hierarchy from home.
NavigationLink("Hello A!!!", destination: AView())
NavigationLink("Greetings B!!!", destination: BView())
}
}
}
}
// Just made two simple views.
struct AView: View {
var body: some Scene {
VStack {
Text("Hello from A")
}.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .center)
.background(Color.blue)
}
}
struct BView: View {
var body: some Scene {
VStack {
Text("Hello from B")
}.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .center)
.background(Color.blue)
}
}
If you launch your app now, you will see home with two buttons, tapping on it triggers the navigation in the app.
As you want to force the app to navigate directly to one of the items, you will need to provide identifiers you can use to a bound variable, triggering the navigation automatically. What does that mean?
// let's create accessors to those links that you can reference from
// somewhere in your code.
// Perhaps, you have a view model maintaining this state.
enum Details {
case a
case b
}
// And let's adapt home view, with the appropriate state tracking
// the "tag"
struct HomeView: View {
#State var selectedTag: Details? = nil
var body: some View {
NavigationView {
ScrollView {
NavigationLink(
"Hello A!!!",
destination: AView(),
tag: Details.a,
selection: self.$selected)
NavigationLink(
"Greetings B!!!",
destination: BView(),
tag: Details.b,
selection: self.$selected)
}
}
}
}
If you launch your app, it will be pretty much as before, but now you can provide a value to that state variable called selectedTag and make it go wherever you want.
struct HomeView: View {
#State var selectedTag: Details? = .a
var body: some View {
// ...
}
}
Launching the app now, the app will display AView with HomeView stacked in the navigation.
Related
I have a CameraView in my app that I'd like to bring up whenever a button is to be presssed. It's a custom view that looks like this
// The CameraView
struct Camera: View {
#StateObject var model = CameraViewModel()
#State var currentZoomFactor: CGFloat = 1.0
#Binding var showCameraView: Bool
// MARK: [main body starts here]
var body: some View {
GeometryReader { reader in
ZStack {
// This black background lies behind everything.
Color.black.edgesIgnoringSafeArea(.all)
CameraViewfinder(session: model.session)
.onAppear {
model.configure()
}
.alert(isPresented: $model.showAlertError, content: {
Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
model.alertError.primaryAction?()
}))
})
.scaledToFill()
.ignoresSafeArea()
.frame(width: reader.size.width,height: reader.size.height )
// Buttons and controls on top of the CameraViewfinder
VStack {
HStack {
Button {
//
} label: {
Image(systemName: "xmark")
.resizable()
.frame(width: 20, height: 20)
.tint(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
Spacer()
flashButton
}
HStack {
capturedPhotoThumbnail
Spacer()
captureButton
Spacer()
flipCameraButton
}
.padding([.horizontal, .bottom], 20)
.frame(maxHeight: .infinity, alignment: .bottom)
}
} // [ZStack Ends Here]
} // [Geometry Reader Ends here]
} // [Main Body Ends here]
// More view component code goes here but I've excluded it all for brevity (they don't add anything substantial to the question being asked.
} // [End of CameraView]
It contains a CameraViewfinder View which conforms to the UIViewRepresentable Protocol:
struct CameraViewfinder: UIViewRepresentable {
class VideoPreviewView: UIView {
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}
let session: AVCaptureSession
func makeUIView(context: Context) -> VideoPreviewView {
let view = VideoPreviewView()
view.backgroundColor = .black
view.videoPreviewLayer.cornerRadius = 0
view.videoPreviewLayer.session = session
view.videoPreviewLayer.connection?.videoOrientation = .portrait
return view
}
func updateUIView(_ uiView: VideoPreviewView, context: Context) {
}
}
I wish to add a binding property to this camera view that allows me to toggle this view in and out of my screen like any other social media app would allow. Here's an example
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView(showCamera: $showCamera)
}
}
I understand that the code to achieve this must be written inside the updateUIView() method. Now, although I'm quite familiar with SwiftUI, I'm relatively inexperienced with UIKit, so any help on this and any helpful resources that could help me better code situations similar to this would be greatly appreciated.
Thank you.
EDIT: Made it clear that the first block of code is my CameraView.
EDIT2: Added Example of how I'd like to use the CameraView in my App.
Judging by the way you would like to use it in the app, the issue seems to not be with the CameraViewFinder but rather with the way in which you want to present it.
A proper SwiftUI way to achieve this would be to use a sheet like this:
#State var showCamera: Bool = false
var body: some View {
mainTabView
.sheet(isPresented: $showCamera) {
CameraView()
.interactiveDismissDisabled() // Disables swipe to dismiss
}
}
If you don't want to use the sheet presentation and would like to cover the whole screen instead, then you should use the .fullScreenCover() modifier like this.
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView()
.fullScreenCover(isPresented: $showCamera)
}
}
Either way you would need to somehow pass the state to your CameraView to allow the presented screen to set the state to false and therefore dismiss itself, e.g. with a button press.
My SwiftUI TVOS app has two sets of NavigationLink. When both sets are present (not commented out), only one set is accessible to tap on. If I comment out one or the other set, the remaining NavigationLink is accessible to tap on and functions properly.
How can both sets of NavigationLink be accessible (can be interacted with)?
I've tried encapsulating my view in NavigationView and NavigationStack, neither behaved differently.
The view, as shown below, only the NavigationLinks in the ScrollView are accessible to interact with. The "Edit" NavigationLink cannot be selected to tap on. If I comment out the ScrollView NavigationLinks, then the "Edit" NavigationLink becomes accessible and functions correctly.
I've also tried replacing LazyVGrid with VStack to no effect.
import SwiftUI
struct TestSources: Hashable {
let id = UUID()
let name: String
}
struct SourcesView: View {
private var Sources = [TestSources(name: "Computer 1"), TestSources(name: "Computer 2")]
var columns: [GridItem] {
Array(repeating: .init(.adaptive(minimum: 200)), count: 2)
}
var body: some View {
NavigationStack {
VStack(alignment: .center) {
// Header
HStack(alignment: .center){
Label("Sources", systemImage: "externaldrive.connected.to.line.below")
.font(.headline)
.frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
.padding(.all)
NavigationLink(destination: TestEditView()) {
Text("Edit")
}
}
Divider()
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(Sources.indices, id: \.self) { index in
NavigationLink(Sources[index].name ,value: Sources[index])
}.navigationDestination(for: TestSources.self) { source in
TestShareView(source: source)
}
.accentColor(Color.black)
.padding(Edge.Set.vertical, 20)
}
.padding(.horizontal)
}
}.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
}
}
}
struct TestEditView: View {
var body: some View {
Text("Edit")
}
}
struct TestShareView: View {
let source : TestSources
var body: some View {
Text(source.name)
}
}
I don't see any problem with the navigation links in this code.
I pasted the code into a new project and tweaked it a little to make it compile. As you can see, it just works.
My guess is that it might fail because something outside of this code. Maybe, it is within another NavigationStack or some structure that could increse it's navigation complexity?
Or as Yrb suggests, this force unwrapping could be failing because of null values?
I have a pretty usual app with a TabView. However, when a particular process is happening in one of the content views, I would like to prevent the user from switching tabs until that process is complete.
If I use the disabled property on the TabView itself (using a #State binding to drive it), then the entire content view seems disabled - taps don't appear to be getting through to buttons on the main view.
Example:
struct FooView: View {
var body: some View {
TabView {
View1().tabItem(...)
View2().tabItem(...)
}
.disabled(someStateVal)
}
}
Obviously, I want the View1 to still allow the user to, you know, do things. When someStateVal is true, the entire View1 doesn't respond.
Is there a way to prevent changing tabs based on someStateVal?
Thanks!
I could not find a way to individually disable a tabItem, so here is
an example idea until someone comes up with more principled solution.
The trick is to cover the tab bar with a clear rectangle to capture the taps.
struct ContentView: View {
#State var isBusy = false
var body: some View {
ZStack {
TabView {
TestView(isBusy: $isBusy)
.tabItem {Image(systemName: "globe")}
Text("textview 2")
.tabItem {Image(systemName: "info.circle")}
Text("textview 3")
.tabItem {Image(systemName: "gearshape")}
}
VStack {
Spacer()
if isBusy {
Rectangle()
.fill(Color.white.opacity(0.001))
.frame(width: .infinity, height: 50)
}
}
}
}
}
struct TestView: View {
#Binding var isBusy: Bool
var body: some View {
VStack {
Text("TestView")
Button(action: {
isBusy.toggle()
}) {
Text("Busy \(String(isBusy))").frame(width: 170, height: 70)
}
}
}
}
I use another trick. Just hide the tab image.
struct FooView: View {
var body: some View {
TabView {
View1().tabItem{Image(systemName: someStateVal ? "": "globe")}
View2().tabItem{Image(systemName: someStateVal ? "": "gearshape")}
}
}
}
I have the scenario where I intend on using a menu ('MenuView') that varies if the device is in portrait / landscape mode. I am using size classes to determine the device type and this successfully redraws the view on rotation. The menu uses navigation view/links to take you to a further view (the 'DetailedView'); this view also has differing views for portrait and landscape. Again I'm using size classes to successfully redraw the view based on the rotation.
However, what I find is that when I'm in the DetailedView and rotate the device, the display jumps straight back to the MenuView as, of course, it has recognized the size class change and adjusted that view. I would like the display to remain in this view.
How can I prevent the app from jumping to the 'MenuView' when I rotate the device that is displaying the 'DetailedView'? Code from the MenuView below:
Note: I'm using the StackNavigationViewStyle in this instance.
Any help would be gratefully received, thanks in advance!
struct MenuView: View {
#Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
#Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
var body: some View {
NavigationView {
Color("MainBg")
.edgesIgnoringSafeArea(.all)
if horizontalSizeClass == .compact {
PortraitMenuView()
} else {
LandscapeMenuView()
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
Update: DetailedView - This is called from the PortraitMenuView and LandscapeMenuView. Included code from PortraitMenuView - the same navigation link is included in the LandscapeMenuView
struct DetailedView: View {
#Environment(\.horizontalSizeClass) var sizeHClass
#Environment(\.verticalSizeClass) var sizeVClass
var formulae: Functions
var body: some View {
ZStack {
Color.offWhite
.edgesIgnoringSafeArea(.all)
VStack {
Group {
if sizeHClass == .compact && sizeVClass == .compact {
DetailedViewLandscape()
} else {
DetailedViewPortrait()
}
}
}
}
}
}
struct PortraitMenuView: View {
var body: some View {
VStack {
ZStack {
Circle()
.fill(Color("kMainBg"))
.frame(width: 300, height: 300)
.overlay(
Text("FORMULA FINDER")
.font(.largeTitle).bold()
.multilineTextAlignment(.center)
.minimumScaleFactor(0.005)
.lineLimit(2)
.frame(width: 210, height: 210)
)
}
Spacer()
NavigationLink(destination: DetailedView()) {
TileView(title: "Formulae", subtitle: "Functions and formulas", boxColor: Color.pastelGreen)
}.offset(x: 40)
}.padding(.bottom, 20)
}
}
I have a:
contentView()
SignUpView()
SignInView()
The contentView calls the SignInView()
struct ContentView: View {
var body: some View {
NavigationView {
SignInView()
}
}
}
In my SignUpView() I have:
var body: some View {
VStack(alignment: .leading) {
NavigationLink(destination: SignInView()) {
Text("Sign in")
.fontWeight(.semibold)
.foregroundColor(Color("startColor"))
}
}.navigationBarHidden(true)
In my SigbInView I have:
var body: some View {
VStack(alignment: .leading) {
NavigationLink(destination: SignUpView()) {
Text("Sign up")
.fontWeight(.semibold)
.foregroundColor(Color("startColor"))
}.navigationBarHidden(true)
Im using .navigationBarHidden(true) to hide the bar, but the < back still appears in the top left hand corner to take you back to the previous screen, Iv also tried adding the navbar text = "" and setting the property to .inline
Im trying to only use these navigationLinks on the SignInView and SignUpViews to navigate, i don't want the bar to appear or push the view down.
So it looks like another property can be set to true to hide the back button:
.navigationBarBackButtonHidden(true)
This worked for me.