คำถามสัมภาษณ์ SwiftUI NavigationStack: รูปแบบการนำทาง 2026
เตรียมตัวสัมภาษณ์ iOS ด้วยคำถามสำคัญเกี่ยวกับ NavigationStack, NavigationPath และรูปแบบการนำทางสมัยใหม่ใน SwiftUI

การนำทางเป็นเสาหลักสำคัญของแอปพลิเคชัน iOS ทุกตัว ตั้งแต่ iOS 16 เป็นต้นมา NavigationStack ได้เข้ามาแทนที่ NavigationView และมอบการควบคุมเชิงโปรแกรมเต็มรูปแบบเหนือสแต็กการนำทาง นายจ้างมักจะทดสอบความเชี่ยวชาญในแนวคิดเหล่านี้ในระหว่างการสัมภาษณ์ทางเทคนิคเป็นประจำ
แต่ละคำถามจำลองรูปแบบของการสัมภาษณ์ทางเทคนิคจริง พร้อมคำตอบโดยละเอียดและโค้ดที่ใช้งานได้ เนื้อหาจะค่อย ๆ เพิ่มความซับซ้อนจากระดับพื้นฐานไปถึงระดับสูง
พื้นฐานของ NavigationStack
คำถามที่ 1: NavigationView และ NavigationStack ต่างกันอย่างไร?
NavigationView (เลิกใช้ตั้งแต่ iOS 16) สร้างการนำทางแบบโดยปริยายโดยอิงจาก NavigationLink ที่ซ้อนกัน ส่วน NavigationStack แนะนำแนวทางแบบเชิงประกาศพร้อมสแต็กการนำทางที่ชัดเจนและสามารถปรับเปลี่ยนได้ด้วยโปรแกรม
// ❌ Old pattern with NavigationView (deprecated)
struct OldNavigation: View {
var body: some View {
NavigationView {
NavigationLink("Details", destination: DetailView())
}
}
}
// ✅ New pattern with NavigationStack
struct ModernNavigation: View {
// Navigation stack is explicit and controllable
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List {
// NavigationLink with typed value
NavigationLink("User 1", value: User(id: 1, name: "Alice"))
NavigationLink("User 2", value: User(id: 2, name: "Bob"))
}
// Destination defined by value type
.navigationDestination(for: User.self) { user in
UserDetailView(user: user)
}
}
}
}ข้อได้เปรียบหลักคือการแยกการประกาศลิงก์ออกจากปลายทาง ซึ่งช่วยให้การนำทางเป็นแบบรวมศูนย์และทดสอบได้
คำถามที่ 2: NavigationPath ทำงานอย่างไร?
NavigationPath คือคอนเทนเนอร์แบบลบประเภท (type-erased) ที่จัดเก็บค่าการนำทาง สามารถจัดการสแต็กได้โดยไม่จำเป็นต้องรู้ประเภทที่แน่นอนของหน้าจอ ขณะเดียวกันก็คงความปลอดภัยของประเภทไว้ในเวลาคอมไพล์
struct ContentView: View {
// NavigationPath can contain different Hashable types
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
VStack(spacing: 20) {
Button("View user profile") {
// Adds a User to the stack
path.append(User(id: 1, name: "Alice"))
}
Button("View settings") {
// Adds a Settings enum to the stack
path.append(SettingsRoute.notifications)
}
Button("Back to root") {
// Clears the entire stack
path.removeLast(path.count)
}
}
.navigationDestination(for: User.self) { user in
UserDetailView(user: user)
}
.navigationDestination(for: SettingsRoute.self) { route in
SettingsView(route: route)
}
}
}
}
// Types must be Hashable
struct User: Hashable {
let id: Int
let name: String
}
enum SettingsRoute: Hashable {
case notifications
case privacy
case account
}NavigationPath ใช้การลบประเภทภายในแต่ตรวจสอบประเภทในเวลาคอมไพล์ผ่าน navigationDestination ค่าที่ไม่มีปลายทางที่สอดคล้องกันจะทำให้เกิดข้อผิดพลาดเงียบในเวลารัน
คำถามที่ 3: จะนำการนำทางเชิงโปรแกรมที่สมบูรณ์ไปใช้อย่างไร?
การนำทางเชิงโปรแกรมช่วยให้ควบคุมสแต็กได้จากจุดใดก็ได้ในโค้ด โดยไม่ต้องอาศัยการกระทำของผู้ใช้โดยตรง สิ่งนี้สำคัญสำหรับ deep link, การเปลี่ยนเส้นทางหลังการยืนยันตัวตน หรือโฟลว์หลายขั้นตอน
// Centralized router to manage navigation
@Observable
class NavigationRouter {
var path = NavigationPath()
// Navigate to a specific screen
func navigateTo(_ destination: AppRoute) {
path.append(destination)
}
// Go back one level
func goBack() {
guard !path.isEmpty else { return }
path.removeLast()
}
// Return to root
func popToRoot() {
path.removeLast(path.count)
}
// Navigate to a complete stack (deep link)
func navigateToPath(_ routes: [AppRoute]) {
popToRoot()
for route in routes {
path.append(route)
}
}
}
// Enum defining all app routes
enum AppRoute: Hashable {
case userList
case userDetail(userId: Int)
case userEdit(userId: Int)
case settings
case settingsDetail(SettingsSection)
}
enum SettingsSection: String, Hashable {
case notifications, privacy, account
}
// Usage in main view
struct MainView: View {
@State private var router = NavigationRouter()
var body: some View {
NavigationStack(path: $router.path) {
HomeView()
.navigationDestination(for: AppRoute.self) { route in
destinationView(for: route)
}
}
.environment(router)
}
@ViewBuilder
private func destinationView(for route: AppRoute) -> some View {
switch route {
case .userList:
UserListView()
case .userDetail(let userId):
UserDetailView(userId: userId)
case .userEdit(let userId):
UserEditView(userId: userId)
case .settings:
SettingsView()
case .settingsDetail(let section):
SettingsDetailView(section: section)
}
}
}รูปแบบนี้รวมศูนย์ตรรกะการนำทางทั้งหมด ทำให้การทดสอบหน่วยและการบำรุงรักษาง่ายขึ้น
รูปแบบการนำทางขั้นสูง
คำถามที่ 4: จะนำ deep link มาใช้กับ NavigationStack อย่างไร?
Deep link ช่วยเปิดแอปพลิเคชันไปยังหน้าจอที่ระบุได้โดยตรงจาก URL ภายนอก ด้วย NavigationStack สามารถสร้างสแต็กขึ้นใหม่ได้ด้วยโปรแกรม
@Observable
class DeepLinkHandler {
var router: NavigationRouter
init(router: NavigationRouter) {
self.router = router
}
// Parse a URL and navigate to destination
func handle(url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let host = components.host else {
return
}
// Build navigation stack according to URL
let routes = parseRoutes(host: host, path: components.path, queryItems: components.queryItems)
router.navigateToPath(routes)
}
private func parseRoutes(host: String, path: String, queryItems: [URLQueryItem]?) -> [AppRoute] {
// myapp://users/42/edit → [.userList, .userDetail(42), .userEdit(42)]
switch host {
case "users":
return parseUserPath(path)
case "settings":
return parseSettingsPath(path)
default:
return []
}
}
private func parseUserPath(_ path: String) -> [AppRoute] {
let segments = path.split(separator: "/").map(String.init)
var routes: [AppRoute] = [.userList]
if let userIdString = segments.first, let userId = Int(userIdString) {
routes.append(.userDetail(userId: userId))
// /users/42/edit
if segments.count > 1 && segments[1] == "edit" {
routes.append(.userEdit(userId: userId))
}
}
return routes
}
private func parseSettingsPath(_ path: String) -> [AppRoute] {
var routes: [AppRoute] = [.settings]
let segments = path.split(separator: "/").map(String.init)
if let sectionString = segments.first,
let section = SettingsSection(rawValue: sectionString) {
routes.append(.settingsDetail(section))
}
return routes
}
}
// In the main App
@main
struct MyApp: App {
@State private var router = NavigationRouter()
@State private var deepLinkHandler: DeepLinkHandler?
var body: some Scene {
WindowGroup {
MainView()
.environment(router)
.onOpenURL { url in
// Handle deep links
deepLinkHandler?.handle(url: url)
}
.onAppear {
deepLinkHandler = DeepLinkHandler(router: router)
}
}
}
}คำถามที่ 5: จะเก็บและกู้คืนสถานะการนำทางอย่างไร?
การเก็บสถานะการนำทางช่วยให้กู้คืนตำแหน่งของผู้ใช้ได้หลังจากเริ่มต้นแอปใหม่ NavigationPath รองรับ Codable สำหรับการซีเรียลไลเซชัน
// Extension to make NavigationPath persistable
extension NavigationPath {
// Encode path to Data
func encoded() -> Data? {
guard let representation = self.codable else { return nil }
return try? JSONEncoder().encode(representation)
}
// Decode from Data
static func decoded(from data: Data) -> NavigationPath? {
guard let representation = try? JSONDecoder().decode(
NavigationPath.CodableRepresentation.self,
from: data
) else {
return nil
}
return NavigationPath(representation)
}
}
// Router with persistence
@Observable
class PersistentNavigationRouter {
var path: NavigationPath {
didSet {
saveState()
}
}
private let storageKey = "navigation_path"
init() {
// Restore state at startup
if let data = UserDefaults.standard.data(forKey: storageKey),
let restored = NavigationPath.decoded(from: data) {
self.path = restored
} else {
self.path = NavigationPath()
}
}
private func saveState() {
if let data = path.encoded() {
UserDefaults.standard.set(data, forKey: storageKey)
}
}
func clearPersistedState() {
UserDefaults.standard.removeObject(forKey: storageKey)
}
}เพื่อให้ NavigationPath.codable ทำงานได้ ทุกประเภทที่เพิ่มลงใน path ต้องเป็น Codable นอกจาก Hashable มิฉะนั้นพร็อพเพอร์ตี codable จะคืนค่า nil
พร้อมที่จะพิชิตการสัมภาษณ์ iOS แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
คำถามที่ 6: จะจัดการการนำทางร่วมกับโฟลว์การยืนยันตัวตนอย่างไร?
โฟลว์การยืนยันตัวตนมักต้องเปลี่ยนเส้นทางไปยังหน้าจอที่ได้รับการป้องกันหลังจากเข้าสู่ระบบ หรือกลับไปยังหน้าเข้าสู่ระบบหลังออกจากระบบ รูปแบบต่อไปนี้จัดการการเปลี่ยนผ่านเหล่านี้ได้อย่างเรียบร้อย
enum AuthState {
case unauthenticated
case authenticated(User)
}
@Observable
class AuthManager {
var state: AuthState = .unauthenticated
var pendingDeepLink: URL?
func login(email: String, password: String) async throws {
// Simulated authentication
let user = try await AuthService.shared.login(email: email, password: password)
state = .authenticated(user)
}
func logout() {
state = .unauthenticated
}
}
struct RootView: View {
@State private var authManager = AuthManager()
@State private var router = NavigationRouter()
var body: some View {
Group {
switch authManager.state {
case .unauthenticated:
// Separate navigation stack for auth
AuthNavigationStack(authManager: authManager)
case .authenticated:
// Main app stack
MainNavigationStack(router: router, authManager: authManager)
}
}
.onChange(of: authManager.state) { oldState, newState in
handleAuthStateChange(from: oldState, to: newState)
}
}
private func handleAuthStateChange(from oldState: AuthState, to newState: AuthState) {
switch (oldState, newState) {
case (.unauthenticated, .authenticated):
// Login successful: process pending deep link
if let pendingURL = authManager.pendingDeepLink {
DeepLinkHandler(router: router).handle(url: pendingURL)
authManager.pendingDeepLink = nil
}
case (.authenticated, .unauthenticated):
// Logout: reset navigation
router.popToRoot()
default:
break
}
}
}
struct AuthNavigationStack: View {
let authManager: AuthManager
@State private var authPath = NavigationPath()
var body: some View {
NavigationStack(path: $authPath) {
LoginView(authManager: authManager)
.navigationDestination(for: AuthRoute.self) { route in
switch route {
case .register:
RegisterView(authManager: authManager)
case .forgotPassword:
ForgotPasswordView()
}
}
}
}
}
enum AuthRoute: Hashable {
case register
case forgotPassword
}คำถามที่ 7: จะนำการนำทางแบบโมดอลไปใช้กับ NavigationStack อย่างไร?
โมดอลและ sheet ต้องมีบริบทการนำทางของตัวเอง การรวม NavigationStack กับการแสดงผลแบบโมดอลจึงต้องการการจัดการสถานะที่แยกออกจากกัน
struct ParentView: View {
@State private var mainPath = NavigationPath()
@State private var showSettings = false
@State private var showUserProfile: User?
var body: some View {
NavigationStack(path: $mainPath) {
ContentView()
.toolbar {
Button("Settings") {
showSettings = true
}
}
.navigationDestination(for: MainRoute.self) { route in
MainRouteView(route: route)
}
}
// Sheet with its own NavigationStack
.sheet(isPresented: $showSettings) {
SettingsSheet()
}
// Conditional sheet based on item
.sheet(item: $showUserProfile) { user in
UserProfileSheet(user: user)
}
}
}
// Each sheet has its own NavigationStack
struct SettingsSheet: View {
@Environment(\.dismiss) private var dismiss
@State private var settingsPath = NavigationPath()
var body: some View {
NavigationStack(path: $settingsPath) {
SettingsListView()
.navigationTitle("Settings")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Close") {
dismiss()
}
}
}
.navigationDestination(for: SettingsSection.self) { section in
SettingsDetailView(section: section)
}
}
}
}
// Extension to make User identifiable for sheet(item:)
extension User: Identifiable {}การจัดการสถานะและความสามารถในการทดสอบ
คำถามที่ 8: จะเขียน unit test สำหรับการนำทางอย่างไร?
ความสามารถในการทดสอบเป็นข้อได้เปรียบสำคัญของ NavigationStack การแยก router ออกมาช่วยให้ unit test สามารถตรวจสอบตรรกะการนำทางได้โดยไม่ต้องใช้ UI
// Protocol to abstract the router
protocol NavigationRouterProtocol {
var pathCount: Int { get }
func navigateTo(_ destination: AppRoute)
func goBack()
func popToRoot()
}
// Concrete implementation
@Observable
class AppNavigationRouter: NavigationRouterProtocol {
var path = NavigationPath()
var pathCount: Int {
path.count
}
func navigateTo(_ destination: AppRoute) {
path.append(destination)
}
func goBack() {
guard !path.isEmpty else { return }
path.removeLast()
}
func popToRoot() {
path.removeLast(path.count)
}
}
// Unit tests
import XCTest
final class NavigationRouterTests: XCTestCase {
var router: AppNavigationRouter!
override func setUp() {
router = AppNavigationRouter()
}
func testNavigateToAddsToPath() {
// Given
XCTAssertEqual(router.pathCount, 0)
// When
router.navigateTo(.userDetail(userId: 42))
// Then
XCTAssertEqual(router.pathCount, 1)
}
func testGoBackRemovesLastItem() {
// Given
router.navigateTo(.userList)
router.navigateTo(.userDetail(userId: 1))
XCTAssertEqual(router.pathCount, 2)
// When
router.goBack()
// Then
XCTAssertEqual(router.pathCount, 1)
}
func testPopToRootClearsPath() {
// Given
router.navigateTo(.userList)
router.navigateTo(.userDetail(userId: 1))
router.navigateTo(.userEdit(userId: 1))
XCTAssertEqual(router.pathCount, 3)
// When
router.popToRoot()
// Then
XCTAssertEqual(router.pathCount, 0)
}
func testGoBackOnEmptyPathDoesNothing() {
// Given
XCTAssertEqual(router.pathCount, 0)
// When
router.goBack()
// Then
XCTAssertEqual(router.pathCount, 0) // No crash
}
}คำถามที่ 9: จะจัดการสถานะการนำทางที่ซับซ้อนเมื่อมีหลายแท็บอย่างไร?
แอปพลิเคชันที่มี TabView ต้องการสถานะการนำทางสำหรับแต่ละแท็บ แต่ละแท็บจะคงสแต็กที่เป็นอิสระของตัวเองไว้
// Navigation state for each tab
@Observable
class TabNavigationState {
var homePath = NavigationPath()
var searchPath = NavigationPath()
var profilePath = NavigationPath()
var selectedTab: Tab = .home
enum Tab: Hashable {
case home, search, profile
}
func resetCurrentTab() {
switch selectedTab {
case .home:
homePath.removeLast(homePath.count)
case .search:
searchPath.removeLast(searchPath.count)
case .profile:
profilePath.removeLast(profilePath.count)
}
}
func resetAllTabs() {
homePath.removeLast(homePath.count)
searchPath.removeLast(searchPath.count)
profilePath.removeLast(profilePath.count)
}
}
struct TabRootView: View {
@State private var tabState = TabNavigationState()
var body: some View {
TabView(selection: $tabState.selectedTab) {
// Home tab
NavigationStack(path: $tabState.homePath) {
HomeView()
.navigationDestination(for: HomeRoute.self) { route in
HomeRouteView(route: route)
}
}
.tabItem { Label("Home", systemImage: "house") }
.tag(TabNavigationState.Tab.home)
// Search tab
NavigationStack(path: $tabState.searchPath) {
SearchView()
.navigationDestination(for: SearchRoute.self) { route in
SearchRouteView(route: route)
}
}
.tabItem { Label("Search", systemImage: "magnifyingglass") }
.tag(TabNavigationState.Tab.search)
// Profile tab
NavigationStack(path: $tabState.profilePath) {
ProfileView()
.navigationDestination(for: ProfileRoute.self) { route in
ProfileRouteView(route: route)
}
}
.tabItem { Label("Profile", systemImage: "person") }
.tag(TabNavigationState.Tab.profile)
}
.environment(tabState)
}
}คำถามที่ 10: จะหลีกเลี่ยงปัญหาประสิทธิภาพของ NavigationStack อย่างไร?
สแต็กการนำทางขนาดใหญ่หรือปลายทางที่ซับซ้อนอาจส่งผลต่อประสิทธิภาพ มีหลายเทคนิคที่ช่วยปรับการเรนเดอร์และการใช้หน่วยความจำให้เหมาะสม
struct OptimizedNavigationStack: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
LazyContentView()
// ✅ Lazy-loaded destinations
.navigationDestination(for: HeavyRoute.self) { route in
// View only created on navigation
HeavyDetailView(route: route)
}
}
}
}
// ✅ View with lazy content loading
struct LazyContentView: View {
@State private var items: [Item] = []
var body: some View {
// LazyVStack only creates visible views
ScrollView {
LazyVStack(spacing: 12) {
ForEach(items) { item in
NavigationLink(value: HeavyRoute.detail(item.id)) {
ItemRow(item: item)
}
}
}
}
.task {
items = await loadItems()
}
}
}
// ✅ Detail with progressive loading
struct HeavyDetailView: View {
let route: HeavyRoute
@State private var data: DetailData?
var body: some View {
Group {
if let data {
DetailContent(data: data)
} else {
ProgressView()
}
}
.task {
// Load data only when view appears
data = await loadDetailData(for: route)
}
}
}
// ❌ Avoid: eager creation of heavy views
struct BadNavigationStack: View {
let allItems: [Item]
var body: some View {
NavigationStack {
List(allItems) { item in
// Creates all destinations immediately
NavigationLink {
HeavyDetailView(item: item) // ❌ Created upfront
} label: {
ItemRow(item: item)
}
}
}
}
}ควรใช้ navigationDestination(for:) แทน NavigationLink ที่มีปลายทางแบบอินไลน์เสมอ รูปแบบแรกจะโหลดวิวปลายทางเฉพาะเมื่อเกิดการนำทางจริงเท่านั้น
บทสรุป
NavigationStack เปลี่ยนวิธีจัดการการนำทางใน SwiftUI ด้วยการมอบการควบคุมเชิงโปรแกรมที่สมบูรณ์ การเชี่ยวชาญรูปแบบเหล่านี้ ตั้งแต่การนำทางพื้นฐานไปจนถึง deep link และการเก็บสถานะ ทำให้นักพัฒนา iOS ที่มีประสบการณ์โดดเด่นในการสัมภาษณ์
รายการตรวจสอบ
- ✅ เข้าใจความแตกต่างระหว่าง
NavigationViewและNavigationStack - ✅ ใช้
NavigationPathสำหรับการนำทางเชิงโปรแกรมเป็น - ✅ ติดตั้ง router แบบรวมศูนย์สำหรับการนำทาง
- ✅ จัดการ deep link พร้อมการสร้างสแต็กขึ้นใหม่
- ✅ เก็บและกู้คืนสถานะการนำทางด้วย
Codable - ✅ แยกโฟลว์การยืนยันตัวตนออกจากการนำทางหลัก
- ✅ ใช้โมดอลร่วมกับ
NavigationStackได้อย่างถูกต้อง - ✅ เขียน unit test สำหรับตรรกะการนำทาง
- ✅ จัดการการนำทางแบบหลายแท็บด้วย
TabView - ✅ ปรับประสิทธิภาพสำหรับสแต็กการนำทางขนาดใหญ่
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
คุณหาบั๊กใน iOS เจอไหม
โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

เขียนโดย
Anthony Fillion-Mailletผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 29 เมษายน 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

สัมภาษณ์ MapKit SwiftUI ในปี 2026: คำอธิบายประกอบ การซ้อนทับ และการระบุตำแหน่ง
เชี่ยวชาญ MapKit ด้วย SwiftUI สำหรับการสัมภาษณ์ iOS: คำอธิบายประกอบแบบกำหนดเอง การซ้อนทับ การระบุตำแหน่ง การค้นหาสถานที่ และรูปแบบการผสานรวมกับ Maps

งาน iOS Developer ปี 2026: แหล่งหางาน ช่วงเงินเดือน และการเตรียมตัวสัมภาษณ์
คู่มือครบถ้วนสำหรับการหางาน iOS Developer ในปี 2026 ค้นพบแหล่งหางานที่ดีที่สุด ข้อมูลเงินเดือนล่าสุด และกลยุทธ์การเตรียมตัวสัมภาษณ์เทคนิค

การสัมภาษณ์ StoreKit 2: การจัดการการสมัครสมาชิกและการตรวจสอบใบเสร็จ
เชี่ยวชาญคำถามสัมภาษณ์ iOS เกี่ยวกับ StoreKit 2 การจัดการการสมัครสมาชิก การตรวจสอบใบเสร็จ และการนำการซื้อในแอปไปใช้ พร้อมตัวอย่างโค้ด Swift ที่ใช้งานได้จริง