본문 바로가기

학습 노트/Swift UI (2022)

22. NavigationView & TabView

NavigationView


 

Apple Developer Documentation

 

developer.apple.com

정리하는 시점에는 이미 사용이 중단돼 NavigationStack으로 대체됐다.

NavigationStack

 

Apple Developer Documentation

 

developer.apple.com

대부분의 modifier는 그대로 사용할 수 있으니 글은 NavigationView를 기준으로 정리하고,
변경된 부분에 대한 설명이 필요할 경우 추가 언급하도록 한다.

앱을 사용하다 보면 아래에서 올라오는 게 아닌, 옆으로 넘어가는 전환 효과와 함께 새로운 화면을 표시하는 경우가 있다.
새로운 화면을 표시하는 과정(왼쪽)을 'Push'라고 부르고,
이전의 화면으로 돌아가는 과정(오른쪽)을 'Pop'이라고 부른다.
이러한 전환 효과(Transition)를 포함해 상단의 현재 Scene의 제목을 표시하는 'Navigation Bar'까지 모두
구성하는 View가 'NavigationView'다.

NavigationLink

 

Apple Developer Documentation

 

developer.apple.com

struct NavigationList: View {
   var body: some View {
      List {
         SupportNavigationLink("Sheet") { Nav_Sheet() }
         SupportNavigationLink("Popover") { Nav_Popover() }
         SupportNavigationLink("Navigation View") { Nav_NavigationView() }
         SupportNavigationLink("Tab View") { Nav_TabView() }
      }
      .navigationBarTitle("Navigation")
   }
}

NavigationView를 호출하기 위해서는 NavigationLink가 필요하다.
사용된 'SupportNavigationLink' 메서드의 구성은 간단하다.

  NavigationLink(destination: Nav_NavigationView()) {
      Text("Navigation View 2")
  }

메서드 내부는 대충 이런 형태로 돼 있다.
NavigationLink를 생성하며, 첫 번째 파라미터로 이동할 View의 생성자를, 두 번째 파라미터로 표시될 이름을 전달한다.

.navigationBarTitle

struct Nav_NavigationView: View {
   @State private var barHidden = false

   var body: some View {
       NavigationView {
           VStack {
               HStack {
                   Text("hidden state: ")
                   Text(barHidden ? "True" : "False")
               }
               .padding()
           }
       }
       .navigationTitle("Test Title")
   }
}

Navigation Bar에 표시될 타이틀을 지정하는 modifier로,
Navigation View에 추가하는 것이 아닌 Navigation View에 embed 된 최종 View에 추가되는 Modifier라는 것이 특징이다.
위 코드에서 NavigationLink로 호출된 Nav_NavigationView가 최종적으로 표시하는 View는 VStack으로,
이에 추가하면 된다.
또한, 이렇게 별도로 지정한 타이틀은 Navigation Link에서 지정한 타이틀보다 우선권이 높다.

.navigationBarHidden

struct Nav_NavigationView: View {
   @State private var barHidden = true
   
   var body: some View {
	   NavigationView {
		   VStack {
			   HStack {
				   Text("hidden state: ")
				   Text(barHidden ? "True" : "False")
			   }
			   .padding()
		   }
	   }
	   .navigationTitle("Test Title")
	   .navigationBarHidden(barHidden)
   }
}

파라미터에 따라 Navigation Bar를 숨기거나 표시할 수 있다.
해당 modifier는 글을 작성하는 시점에 'toolbar'로 대체됐다.

iOS16부터 지원하는 해당 modifier는 아래와 같이 사용한다.

struct Nav_NavigationView: View {
   @State private var barHidden = true
   
   var body: some View {
	   if #available(iOS 16, *) {
		   NavigationView {
			   VStack {
				   HStack {
					   Text("hidden state: ")
					   Text(barHidden ? "True" : "False")
				   }
				   .padding()
			   }
		   }
		   .navigationTitle("Test Title")
		   .toolbar(.hidden, for: .navigationBar)
	   } else {
		   NavigationView {
			   VStack {
				   HStack {
					   Text("hidden state: ")
					   Text(barHidden ? "True" : "False")
				   }
				   .padding()
			   }
		   }
		   .navigationTitle("Test Title")
	   }
   }
}

첫 번째 파라미터는 표시 방식을, 두 번째 파라미터는 적용할 대상을 지정한다.

.navigationBarTitleDisplayMode

NavigationView {
   VStack {
       HStack {
           Text("hidden state: ")
           Text(barHidden ? "True" : "False")
       }
       .padding()
   }
}
.navigationTitle("Test Title")
.navigationBarTitleDisplayMode(.inline)

Navigation Bar에 표시하는 제목의 스타일을 변경할 수 있다.
조금 더 공간을 절약할 수 있는 inline 스타일이 왼쪽,
큰 제목을 표시하는 오른쪽이 largeTtitle이다.

.toolbar

   NavigationView {
       VStack {
           HStack {
               Text("hidden state: ")
               Text(barHidden ? "True" : "False")
           }
           .padding()
       }
   }
   .navigationTitle("Test Title")
   .navigationBarTitleDisplayMode(.large)
   .toolbar {
       Button {

       } label: {
           Label("", systemImage: "1.circle.fill")
       }
   }

Navigation Bar에 버튼을 추가한다.
왼쪽에는 Back button이 자리하고 있기 때문에, 오른쪽에 표시되는 것이 기본이다.

   NavigationView {
       VStack {
           HStack {
               Text("hidden state: ")
               Text(barHidden ? "True" : "False")
           }
           .padding()
       }
   }
   .navigationTitle("Test Title")
   .navigationBarTitleDisplayMode(.large)
   .toolbar {
       HStack {
           Button {
           } label: {
               Label("", systemImage: "1.circle.fill")
           }
           
           Button {
           } label: {
               Label("", systemImage: "2.circle.fill")
           }
           
           Button {
           } label: {
               Label("", systemImage: "3.circle.fill")
           }
           
           Button {
           } label: {
               Label("", systemImage: "4.circle.fill")
           }
           
           Button {
           } label: {
               Label("", systemImage: "5.circle.fill")
           }
           
           Button {
           } label: {
               Label("", systemImage: "6.circle.fill")
           }
           
           Button {
           } label: {
               Label("", systemImage: "7.circle.fill")
           }
           
           Button {
           } label: {
               Label("", systemImage: "8.circle.fill")
           }
           
           Button {
           } label: {
               Label("", systemImage: "9.circle.fill")
           }
           
           Button {
           } label: {
               Label("", systemImage: "10.circle.fill")
           }
       }
   }

복수개의 버튼을 표시하는 것도 가능하지만, 수가 너무 많으면 전부를 표시하지 못하거나,
다른 View를 침범할 수 있다.
이러한 경우 menu를 사용하거나 너비를 제한한 뒤 Scroll View를 사용하는 것을 고려해 볼 수 있다.

.toolBarItemGroup

   if #available(iOS 15, *) {
       NavigationView {
           VStack {
               HStack {
                   Text("hidden state: ")

                   Text(barHidden ? "True" : "False")
               }
               .padding()
           }
       }
       .navigationTitle("Test Title")
       .navigationBarTitleDisplayMode(.large)
       .toolbar {
           ToolbarItemGroup(placement: .bottomBar) {
                   Button {
                       print("1")
                   } label: {
                       Label("first", systemImage: "1.circle.fill")
                   }

                   Divider()

                   Button {
                       print("2")
                   } label: {
                       Label("second", systemImage: "2.circle.fill")
                   }
           }
       }
   }

ToolBar의 위치 등을 변경할 수 있도록 한다.
ToolBarItem의 각각의 설정으로도 가능하지만, 한 번에 묶어 관리할 수 있다는 점이 매력적이다.

같은 코드를 iOS16에서는 bottomBar의 콘텐츠를 제대로 표시하지 못하는 문제가 있는데,
이는 NavigationStack이 등장하면서 생긴 문제이다.
최상위 View의 NavigationView를 NavigationStack으로 교체하는 것으로 간단히 해결할 수 있다.

macOS, iPadOS의 경우

iOS의 NavigationView의 기본 스타일은 StackStyle로 iOS와 iPadOS에서 사용한다.
StackStyle이 아닌 ColumnStyle은 macOS와 iPadOS에서 사용한다.
ColumnStyle은 최대 3개의 View를 동시에 표시할 수 있고,
3개를 넘어가면 마지막 View를 대체하여 표시한다.

   NavigationView {
      List {
         SupportNavigationLink("System Views") { SystemViewList() }
         SupportNavigationLink("Text and Font") { TextAndFontList() }
         SupportNavigationLink("Color") { ColorList() }
         SupportNavigationLink("Image") { ImageList() }
         SupportNavigationLink("Animation") { AnimationList() }
         SupportNavigationLink("List") { ListList() }
         SupportNavigationLink("Navigation") { NavigationList() }
         SupportNavigationLink("State and Binding") { StateAndBindingList() }
         SupportNavigationLink("Gestures") { GestureList() }
         SupportNavigationLink("Core Data") { CoreDataList() }
      }
      .navigationBarTitle("Mastering SwiftUI")

       Text("Hi there!")
   }

NavigationView만 사용할 시 처음 다른 View를 호출하기 전 까지는 빈 화면이 나오게 되므로,
이를 활용해 사용자를 유도하는 등의 사용이 가능하다.

Column Toggle

iOS와 iPadOS는 다른 Column을 터치해 탈출하거나 버튼을 사용할 수 있다.
문제는 macOS에서는 이러한 기능을 자동으로 제공하지 않는다는 점이다.

.toolbar {
    ToolbarItem(placement: .navigation) {
        Button {
            NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
        } label: {
            Label("Toggle sidebar", systemImage: "sidebar.left")
        }
    }
}

이럴 때는 ToolBar에 직접 버튼을 추가해 줘야 할 필요가 있다.
복잡해 보이지만 동작 자체는 간단하다.
toggleSidebar를 firstResponder로 지정하는 방식으로 이를 macOS에서 구현할 수 있다.

 

TabView


 

Apple Developer Documentation

 

developer.apple.com

TabView는 화면의 하단에 표시돼 View 간의 전환을 담당한다.
위치하는 버튼들은 항상 같은 크기로 표시된다.

struct Nav_TabView: View {
   var body: some View {
	   TabView {
		  ImageScene(imageName: "star", color: Color.red)
			   .tabItem {
				   Label("star", systemImage: "star")
			   }
			   .badge(7)
		  ImageScene(imageName: "heart", color: Color.green)
			   .tabItem {
				   Label("heart", systemImage: "heart")
			   }
		  ImageScene(imageName: "play", color: Color.blue)
			   .tabItem {
				   Label("play", systemImage: "play")
			   }
	   }
   }
}

이렇게 간단하게 구현할 수 있다.
주로 사용하는 modifier는 다음과 같다.

.tabItem

.tabItem {
   Label("star", systemImage: "star")
}

위와 같이 tabItem을 구현하지 않아도 동작은 하지만,
어떤 View가 선택됐는지 이를 확인할 indicator를 표시하지 못한다.
보통 Label을 전달해 사용한다.

.badge

.badge(7)

tabItem에 알림 등의 badge를 표시한다.

.tag

보통은 tabItem을 직접 선택해 View를 이동하지만 경우에 따라 코드로 View를 이동해야 할 때가 있다.
이 경우 사용하는 modifier로 각 View에 식별자를 만들어 사용한다.
단, 이 경우 tag가 중복되지 않으며, 명확히 구분되는 값을 사용해야 함에 명심하자.

tab 이동하기

TabView의 파라미터에는 'selction'이라는 파라미터가 존재한다.
해당 파라미터에 값을 전달함으로써 해당 View로 이동하도록 구현하는 것이 가능하다.

struct Nav_TabView: View {
   @State private var selectedIndex = 0
   
   var body: some View {
	   TabView(selection: $selectedIndex) {
		  ImageScene(imageName: "star", color: Color.red)
			   .tabItem {
				   Label("star", systemImage: "star")
			   }
			   .badge(7)
			   .tag(0)
		  ImageScene(imageName: "heart", color: Color.green)
			   .tabItem {
				   Label("heart", systemImage: "heart")
			   }
			   .accentColor(.cyan)
			   .tag(1)
		  ImageScene(imageName: "play", color: Color.blue)
			   .tabItem {
				   Label("play", systemImage: "play")
			   }
			   .tag(2)
	   }
	   .toolbar {
		   Button {
			   selectedIndex = (selectedIndex + 1) % 3
		   } label: {
			   Label("next", systemImage: "forward.fill")
		   }
	   }
   }
}

파라미터에 전달할 State Variable을 하나 선언하고,
앞서 설명한 tag를 사용해 현재 선택된 tab이 어떤 tab인지 구분해 동기화하고,
이를 변경함으로써 tab을 전환한다.

우측 상단의 버튼을 사용해 tab을 전환할 수 있다.

'학습 노트 > Swift UI (2022)' 카테고리의 다른 글

24. Observable Object & Environment Object  (0) 2022.11.01
23. State & Binding  (0) 2022.10.27
21. Animation  (0) 2022.10.07
20. Image & SFSymbol & AsyncImage  (0) 2022.10.06
19. Color & Material  (1) 2022.10.06