SwiftUI) Podcasts clone coding - Menu

3 minute read

내용

  • SwiftUI 사용해서 Podcasts 앱의 클론코딩을 진행했다.
  • 아래의 Menu 를 구현해보자!

해당 컴포넌트는 Menu 이다. 개발자 문서를 통해서 알아보자.

A control for presenting a menu of actions.

Overview

다음 코드는 세 개의 버튼으로 구성된 Menu 와 세 개의 버튼을 포함하는 하위 메뉴를 보여줍니다.

// action 은 커스텀 메서드입니다.
Menu("Actions") {
    Button("Duplicate", action: duplicate)
    Button("Rename", action: rename)
    Button("Delete…", action: delete)
    Menu("Copy") {
        Button("Copy", action: copy)
        Button("Copy Formatted", action: copyFormatted)
        Button("Copy Library Path", action: copyPath)
    }
}

menu 타이틀을 이전 예시처럼 ✅ (참고)LocalizedStringKey(문자열의 localization) 를 사용해서 만들거나, image 나 text 뷰 같은 다수의 뷰를 만드는 view builder 를 사용해서 만들 수 있습니다.

Menu {
    Button("Open in Preview", action: openInPreview)
    Button("Save as PDF", action: saveAsPDF)
} label: {
    Label("PDF", systemImage: "doc.fill")
}

스크린샷 2022-04-26 오후 6 20 45

Primary Action

Menu 는 커스텀 primary action 으로 만들 수 있습니다. primary action 은 사용자가 control 의 body 를 taps 혹은 clicks 했을 때 수행되고 menu presentation(위와 같은 메뉴를 보여주는 기능) 은 long press 혹은 ✅ (참고)menu indicator 를 click 하는 것과 같은 보조 제스처에서 발생합니다.

다음 코드는 menu 에 표시되는 advanced options 와 함께 책갈피를 추가하는 menu 를 만드는 예시입니다.

Menu {
// 🔥 long press 혹은 menu indicator 를 click 해야 등장한다.
    Button(action: addCurrentTabToReadingList) {
        Label("Add to Reading List", systemImage: "eyeglasses")
    }
    Button(action: bookmarkAll) {
        Label("Add Bookmarks for All Tabs", systemImage: "book")
    }
    Button(action: show) {
        Label("Show All Bookmarks", systemImage: "books.vertical")
    }
} label: {
    Label("Add Bookmark", systemImage: "book")
} primaryAction: {
// 🔥 taps 혹은 clicks.
    addBookmark()
}

🚨 에러

🙆‍♂️ 해결: if #available(iOS 15.0, *)

iOS 15 이상부터 primaryAction 을 사용할 수 있었고, #available 를 사용해서 분기처리 해주었습니다.

// 🔥 iOS 15 이상부터 사용가능.
if #available(iOS 15.0, *) {
    Menu {
        Button(action: {}) {
            Label("Add to Reading List", systemImage: "eyeglasses")
        }
        Button(action: {}) {
            Label("Add Bookmarks for All Tabs", systemImage: "book")
        }
        Button(action: {}) {
            Label("Show All Bookmarks", systemImage: "books.vertical")
        }
    } label: {
        Label("Add Bookmark", systemImage: "book")
    } primaryAction: {
        print("primary action")
    }
} else {
    // Fallback on earlier versions
}

Styling Menus

[menuStyle(:)](https://developer.apple.com/documentation/swiftui/view/menustyle(:))modifier 를 사용해서 메뉴의 스타일을 변경할 수 있습니다. 아래의 코드는 custom style 을 적용하는 방법을 보여줍니다.

Menu("Editing") {
    Button("Set In Point", action: setInPoint)
    Button("Set Out Point", action: setOutPoint)
}
// custom style
.menuStyle(EditingControlsMenuStyle())

참고

✅ LocalizedKeyString

  • LocalizedStringKey 인스턴스를 만들어서 문자열을 localize 할 수 있습니다.

UIKit 에서 UILabel 은 String 로 초기화를 합니다. 그러나 SwiftUI 에서 UILabel 역할을 하는 TextLocalizedKeyString 을 통해서 초기화를 합니다. 즉, NSLocalizedString 를 사용하지않아도 자동으로 localization 되는 것입니다.

Menu 역시 LocalizedStringKey 를 사용해서 초기화 할 수 있습니다.

primary action 을 사용하게 되면 menu presentation 은 long press 와 menu indicator 를 통해서 기능한다고 했습니다.

**menuIndicator(_:)**

Sets the menu indicator visibility for controls within this view.

func menuIndicator(_ visibility: Visibility) -> some View

위의 modifier 를 통해서 menu indicator 가 있는 Menu 를 만들 수 있습니다.

  • 화살표와 같은 indicator 가 등장한다고 하는데 왜인지.. 등장하지 않았습니다..!
Menu {
// long press 혹은 menu indicator 를 click 해야 등장한다.
    Button(action: addCurrentTabToReadingList) {
        Label("Add to Reading List", systemImage: "eyeglasses")
    }
    Button(action: bookmarkAll) {
        Label("Add Bookmarks for All Tabs", systemImage: "book")
    }
    Button(action: show) {
        Label("Show All Bookmarks", systemImage: "books.vertical")
    }
} label: {
    Label("Add Bookmark", systemImage: "book")
} primaryAction: {
    addBookmark()
}
// 🔥 menuindicator modifier 를 통해서 가시성을 설정할 수 있다.
.menuIndicator(.visible)

[Apple Developer Documentation - menuindicator(:)](https://developer.apple.com/documentation/swiftui/link/menuindicator(:))


클론코딩

  • Podcast 클론코딩을 진행해보자.

Menu {
    Button(action: {
        print("Download Episode")
    }) {
        Label("Download Episode", systemImage: "arrow.down.circle")
    }
    Button(action: {
        print("Play Episode")
    }) {
        Label("Play Episode", systemImage: "play")
    }

    Divider()

    Button(action: {
            print("Play Next")
        }) {
            Label("Play Next", systemImage: "text.insert")
        }
    Button(action: {
             print("Play Last")
        }) {
            Label("Play Last", systemImage: "text.append")
        }
    Button(action: {
            print("Go to Show")
        }) {
            Label("Go to Show", systemImage: "airplayaudio")
        }
    Button(action: {
            print("Save Episode")
        }) {
            Label("Save Episode", systemImage: "bookmark")
        }

    Divider()

    Button(action: {
        print("Report a Concern")
    }) {
        Label("Report a Concern", systemImage: "exclamationmark.bubble")
    }
    Button(action: {
        print("Copy Link")
    }) {
        Label("Copy Link", systemImage: "link")
    }
    Button(action: {
        print("Share Episode...")
    }) {
        Label("Share Episode...", systemImage: "square.and.arrow.up")
    }
} label: {
        Image(systemName: "ellipsis")
            .foregroundColor(.secondary)
}

에러: Extra argument in call

Podcast 의 Menu 안에는 10개 넘는 Button 들이 필요했고, 다음과 같은 에러를 만나게 되었다. SwiftUI 에서는 View Builder 안에 최대 10개까지만 선언 할 수 있다.

해결: 다음과 같이 파일분할하여 진행하였다.

Divider 를 통해서 구분지었는데 해당 영역마다 역할이 있다면 해당 이름으로 지어도 되지만 순서를 나타내기 위해서 다음과 같이 작성했다.

Menu {
    ThrdMenu()
    Divider()
    SecondMenu()
    Divider()
    FirstMenu()
    } label: {
        Image(systemName: "ellipsis")
            .foregroundColor(.secondary)
    }

// FristMenu
// Divider
// SecondMenu
// Divider
// ThirdMenu
// 순서로 뷰에 그려진다.

결과

(Go to Show 의 아이콘을 SF Symbol 에서 찾지 못했다..ㅜ 그래서 비슷한걸로 대체했습니당)

Categories:

Updated: