관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - SwiftUI] 튜토리얼 - 21. iOS앱 프로젝트에 macOS 맥북 앱 UI 구현 방법 (CommandMenu, SidebarCommands, FocusedValues, Preferences 설정) (5) 본문

iOS 튜토리얼 (SwiftUI)

[iOS - SwiftUI] 튜토리얼 - 21. iOS앱 프로젝트에 macOS 맥북 앱 UI 구현 방법 (CommandMenu, SidebarCommands, FocusedValues, Preferences 설정) (5)

jake-kim 2022. 7. 24. 23:15

* 프로젝트 파일은 애플 튜토리얼 사이트나 이전 포스팅 글 참고

macOS 실행 시 Sidebar가 들어나지 않는 문제 해결 방법

상단에 show Sidebar 옵션 생성 방법

  • macOS를 실행하면 Sidebar를 한 번 닫았을 때 다시 열 수 없는 문제가 존재
    • iOS에서 List형태 안에 NavigationLink로 구현하면 macOS에서는 아래처럼 좌측에는 리스트, 우측에는 NavigationLink에 삽입한 화면이 등장

Sidebar를 한 번 닫으면 다시 열 수 없는 문제

  • LandmarkCommands 라는 파일 생성

  • LandmarkCommands 안에 SidebarCommands()를 선언
import SwiftUI

struct LandmarkCommands: Commands {
    var body: some Commands {
      SidebarCommands()
    }
}
  • LandmarksApp에 .commands를 추가하고, 그 클로저에 LandmarkCommands() 주입
    • 해당 코드를 추가하면 상단 View에 메뉴가 생성
@main
struct LandmarksApp: App {
  @StateObject private var modelData = ModelData()
  
  var body: some Scene {
    WindowGroup {
      ContentView()
        .environmentObject(modelData)
    }
    .commands { // <-
      LandmarkCommands()
    }
    
    #if os(watchOS)
    WKNotificationScene(controller: NotificationController.self, category: "LandmarkNear")
    #endif
  }
}

  • watchOS에는 필요없는 기능이므로 플래그 사용
@main
struct LandmarksApp: App {
  @StateObject private var modelData = ModelData()
  
  var body: some Scene {
    WindowGroup {
      ContentView()
        .environmentObject(modelData)
    }
    
    #if !os(watchOS)  // <-
    .commands {
      LandmarkCommands()
    }
    #endif
    
    #if os(watchOS)
    WKNotificationScene(controller: NotificationController.self, category: "LandmarkNear")
    #endif
  }
}

CommandMenu - 키 설정

  • 커멘드 메뉴 구현 
    • 커멘드는 App을 서브클래싱 하고 있는 곳에서 .commands로 추가
    • commands안에는 Commands를 상속받고 인스턴스(LandmarkCommands)를 주입 
//  LandmarkCommands.swift
struct LandmarkCommands: Commands {
  var body: some Commands {
    SidebarCommands()
  }
}


// LandmarksApp.swift
@main
struct LandmarksApp: App {
  @StateObject private var modelData = ModelData()
  
  var body: some Scene {
    WindowGroup {
      ContentView()
        .environmentObject(modelData)
    }
    
    .commands { // <-
      LandmarkCommands()
    }
    
    ...
  • 키 정의 - 맥북 상단의 메뉴와 스위프트 코드 연동하기위해 정의
    • LandmarkCommands.swift에 FocusedValueKey 키 추가
    • SwiftIU 내부에서 정의된 FocusedValues라는 곳에 extension으로 정의한 키를 computed property로 제공하면 키 정의 완료

https://developer.apple.com/documentation/swiftui/focusedvaluekey
https://developer.apple.com/documentation/swiftui/focusedvalues

private struct SelectedLandmarkKey: FocusedValueKey {
    typealias Value = Binding<Landmark>
}

extension FocusedValues {
    var selectedLandmark: Binding<Landmark>? {
        get { self[SelectedLandmarkKey.self] }
        set { self[SelectedLandmarkKey.self] = newValue }
    }
}
  • CommandMunu를 추가하여 Landmark라는 버튼 추가
    • 여기에서 커멘드가 눌린 경우, 처리를 해줌
    • 사용하는 쪽에서 해당 키값을 선언하고 데이터를 넘기면 처리는 여기서 정의한 코드가 동작하여 데이터에 반영되는 원리

struct LandmarkCommands: Commands {
  @FocusedBinding(\.selectedLandmark) var selectedLandmark
  
  var body: some Commands {
    SidebarCommands()
    
    CommandMenu("Landmark") { // <-
      Button("\(selectedLandmark?.isFavorite == true ? "Remove" : "Mark") as Favorite") {
        selectedLandmark?.isFavorite.toggle()
      }
      .disabled(selectedLandmark == nil)
    }
  }
}
  • 단축키 설정하기
    • .keyboardShortcut으로 추가

struct LandmarkCommands: Commands {
  @FocusedBinding(\.selectedLandmark) var selectedLandmark
  
  var body: some Commands {
    SidebarCommands()
    
    CommandMenu("Landmark") {
      Button("\(selectedLandmark?.isFavorite == true ? "Remove" : "Mark") as Favorite") {
        selectedLandmark?.isFavorite.toggle()
      }
      .keyboardShortcut("f", modifiers: [.shift, .option]) // 단축키 설정: `Shift + Option + F`
      .disabled(selectedLandmark == nil)
    }
  }
}

CommandMenu - 키 바인딩

키 바인딩 완료 - 위 Landmark 커멘드 메뉴에서 Mark as Favorite 선택한 경우 데이터 반영

  • LandmarkList에 특정 코드를 추가하여 데이터 바인딩

1. selectedLandmark 상태를 기록

  • 커멘드 메뉴에서 Mark as Favorite를 선택하면 selectedLandmark에 적용하기 위함
struct LandmarkList: View {
  @EnvironmentObject var modelData: ModelData
  @State private var showFavoritesOnly = false
  @State private var filter = FilterCategory.all
  @State private var selectedLandmark: Landmark? // 1.

...

2. index 프로퍼티 추가

  • 선택한 아이템의 인덱스값을 리턴하는 프로퍼티
  // 2.
  var index: Int? {
    modelData.landmarks.firstIndex(where: { $0.id == selectedLandmark?.id })
  }

3. 기존에 있던 List를 주석처리하고, selection에 위에서 정의한 $selectedLandmark를 주입

  • 리스트에서 현재 선택되어 있는 옵션을 부여
  var body: some View {
    NavigationView {
//      List {
        List(selection: $selectedLandmark) { // 3.

4. .focusedValue로 위에서 정의했던 키값을 바인딩

  • .focusedValue(_:,_:)로, 2가지 값을 주입
  • 첫 번째 인수 keyPath에는 위에서 extension FocusedValues에서 정의한 키값을 사용
  • 두 번째 인수 value에는 첫번째 인수에 전달할 값
  • 두 번째 인수에는 변경될 데이터값을 넣고, 첫 번째 인수에 키패스를 넣으면 키패스에서 정의한 로직대로 데이터가 변경

  var body: some View {
    NavigationView {
    
    ...
    
      Text("Select a Landmark")
    }
    .focusedValue(\.selectedLandmark, $modelData.landmarks[index ?? 0]) // 4.
  }

Preferences, 설정 옵션 추가 방법

  • @AppStorage라는 어노테이션을 통해 키값을 설정하여, 맥 설정 바인딩에 사용
    • 세팅과 관련된 MapView에 @AppStorage를 사용하여 키값 설정 "MapView.zoom"
    • 세팅쪽에도 이와 같은 키값을 사용하고, 이 키값에 해당하는 프로퍼티에 값을 변경하면 여기에도 반영됨
// MapView.swift

struct MapView: View {
  var coordinate: CLLocationCoordinate2D
  
  // 키 값 설정
  @AppStorage("MapView.zoom")
  private var zoom: Zoom = .medium
  
  enum Zoom: String, CaseIterable, Identifiable {
    case near = "Near"
    
  ...
  • 맥을 타겟으로 하는 LandmarkSettings 파일 추가

  • 키값을 추가
struct LandmarkSettings: View {
  // 키값 정의
  @AppStorage("MapView.zoom")
  private var zoom: MapView.Zoom = .medium
  
  var body: some View {
    Text("Hello, World!")
  }
}

struct LandmarkSettings_Previews: PreviewProvider {
  static var previews: some View {
    LandmarkSettings()
  }
}
  • 아래 화면처럼, Form을 사용하여 UI 구현
    • 이 화면은 커멘드 메뉴에서 Preference를 클릭한 경우 등장하는 화면

struct LandmarkSettings: View {
  @AppStorage("MapView.zoom")
  private var zoom: MapView.Zoom = .medium
  
  var body: some View {
    Form { // <-
      Picker("Map Zoom:", selection: $zoom) {
        ForEach(MapView.Zoom.allCases) { level in
          Text(level.rawValue)
        }
      }
      .pickerStyle(.inline)
    }
    .frame(width: 300)
    .navigationTitle("Landmark Settings")
    .padding(80)
  }
}
  • LandmarkApp에 Setting이라는 인스턴스에 정의한 LandmarkSettings() 주입
@main
struct LandmarksApp: App {
  @StateObject private var modelData = ModelData()
  
  var body: some Scene {
    WindowGroup {
      ContentView()
        .environmentObject(modelData)
    }
    
    #if !os(watchOS)
    .commands {
      LandmarkCommands()
    }
    #endif
    
    #if os(watchOS)
    WKNotificationScene(controller: NotificationController.self, category: "LandmarkNear")
    #endif
    
    #if os(macOS) // <-
    Settings {
      LandmarkSettings()
    }
    #endif

  }
}

* 참고

https://developer.apple.com/documentation/swiftui/focusedvalues

https://developer.apple.com/documentation/swiftui/focusedvaluekey

https://developer.apple.com/tutorials/swiftui/creating-a-macos-app

Comments