SwiftUI - 뷰에 하드코딩된 네비게이션을 피하는 방법
더 크고 생산 준비가 된 Swift를 위해 아키텍처 작업을 시도합니다.UI 앱입니다. Swift의 큰 디자인 결함을 지적하는 동일한 문제에 항상 부딪힙니다.UI.
아직 아무도 완벽한 작업 및 생산 준비 답변을 줄 수 없었습니다.
에서 재사용 SwiftUI
비게게네
SwiftUI
NavigationLink
이는 단순히 더 큰 앱에서도 확장될 수 없다는 견해에 강하게 얽매여 있습니다. NavigationLink
이러한 작은 샘플 앱에서는 네, 하지만 한 앱에서 많은 보기를 재사용하고 싶을 때는 그렇지 않습니다.등에서의 등 iOS, Watch OS 표시)
설계상의 문제:NavigationLinks는 보기에 하드 코딩됩니다.
NavigationLink(destination: MyCustomView(item: item))
, 이 뷰가 NavigationLink
재사용이 가능해야 합니다.목적지를 하드코드로 지정할 수 없습니다.목적지를 제공하는 메커니즘이 있어야 합니다.여기서 물어보니 꽤 좋은 답변을 얻었지만, 여전히 완전한 답변을 얻지는 못했습니다.
SwiftUI MVVM 코디네이터/라우터/내비게이션 링크
「Destination Links」는, 「Destination Links」를 참조해 주세요.일반적으로 이 아이디어는 효과가 있지만 안타깝게도 실제 운영 애플리케이션으로 확장되지는 않습니다. 개 , 가능한뷰( 「 」 )가 .ViewA
되어 있는 의 행선지 「」, 「」, 「」, 「」ViewB
단, 의 경우. 하지만 만약ViewB
에는, 미리 되어 있는 뷰 행선지 「」, 「」도 필요합니다.ViewC
을 만들어야 할 것 같아요.ViewB
그렇게 있다ViewC
되어 있다ViewB
하기 ViewB
ViewA
등등... 그러나 그 때 전달해야 할 데이터를 사용할 수 없기 때문에 전체 구축이 실패합니다.
제가 생각한 또 다른 아이디어는Environment
주입 으로서 「Dependency Mechanism」의 를 주입합니다.NavigationLink
하지만 이것은 대규모 앱의 확장 가능한 솔루션이 아니라 해킹으로 간주되어야 한다고 생각합니다.우리는 결국 기본적으로 모든 것에 환경을 사용하게 될 것입니다.그러나 Environment도 View 내부에서만 사용할 수 있기 때문에(별도의 Coordinator나 View Models에서는 사용할 수 없음) 제 의견으로는 다시 이상한 구성 요소가 생성될 수 있습니다.
로직 뷰 코드되어 있어야 되어 있어야 예: 패턴 내에서는 네비게이션과 뷰는되어 있어야 (예: 코디네이터 패턴).UIKit
우리가 할 수 에 가능합니다.UIViewController
★★★★★★★★★★★★★★★★★」UINavigationController
경치 뒤에 UIKit's
MVC는 이미 너무 많은 개념을 혼합하여 "모델 뷰 컨트롤러"가 아닌 "매시브 뷰 컨트롤러"라는 재미있는 이름이 되었습니다..SwiftUI
네, 이렇게요.내비게이션과 보기는 강하게 결합되어 있으며 분리할 수 없습니다.따라서 네비게이션이 포함된 경우 재사용 가능한 보기를 수행할 수 없습니다.이 문제를 해결할 수 있었습니다.UIKit
순간에는 된 이 보이지 않습니다.SwiftUI
유감스럽게도 애플은 우리에게 그와 같은 건축 문제를 해결하는 방법을 설명해주지 않았다.우리는 단지 몇 가지 샘플 앱을 가지고 있을 뿐이다.
내가 틀렸다는 것을 증명하고 싶다.대량생산 가능한 앱의 경우 이를 해결할 수 있는 깔끔한 앱 디자인 패턴을 보여주세요.
업데이트: 이 혜택은 몇 분 안에 종료됩니다.하지만 안타깝게도 아직 아무도 실제 사례를 제시하지 못했습니다.하지만 다른 해결책을 찾아 여기에 연결할 수 없다면 이 문제를 해결하기 위해 새로운 포상금을 시작할 것입니다.여러분의 큰 공헌에 감사드립니다!
업데이트 2020년 6월 18일: Apple로부터 이 문제에 대한 답변을 받고 뷰와 모델을 분리할 것을 제안했습니다.
enum Destination {
case viewA
case viewB
case viewC
}
struct Thing: Identifiable {
var title: String
var destination: Destination
// … other stuff omitted …
}
struct ContentView {
var things: [Thing]
var body: some View {
List(things) {
NavigationLink($0.title, destination: destination(for: $0))
}
}
@ViewBuilder
func destination(for thing: Thing) -> some View {
switch thing.destination {
case .viewA:
return ViewA(thing)
case .viewB:
return ViewB(thing)
case .viewC:
return ViewC(thing)
}
}
}
내 대답은 다음과 같았다.
피드백 감사합니다.그러나 보시다시피 View에는 여전히 강력한 커플링이 있습니다.이제 "ContentView"는 탐색할 수 있는 모든 보기(ViewA, ViewB, ViewC)를 알아야 합니다.말씀드렸듯이, 이것은 소규모 샘플 앱에서는 동작하지만, 대규모 실가동 가능한 앱에서는 확장되지 않습니다.
제가 GitHub의 프로젝트에서 커스텀 뷰를 만든다고 상상해 보세요.그런 다음 이 보기를 내 앱으로 가져옵니다.이 사용자 지정 보기는 앱에 고유하므로 탐색할 수 있는 다른 보기에 대해서도 알 수 없습니다.
제가 그 문제를 더 잘 설명했길 바랍니다.
이 문제를 해결할 수 있는 유일한 방법은 UIKit처럼 내비게이션과 뷰를 분리하는 것입니다(예: UINavigation Controller).
고마워, 다코
따라서 이 문제에 대한 깨끗하고 효과적인 해결책은 아직 없습니다.WWDC 2020을 기대하고 있습니다.
: 2021년 9월 갱신 : 사사사AnyView
는 이 문제에 대한 일반적인 해결책이 아닙니다.빅 앱에서는 기본적으로 모든 뷰를 재사용 가능한 방식으로 설계해야 합니다., '이렇게 하다'라는 입니다.AnyView
어디서든 사용되죠나는 두 명의 애플 개발자들과 세션을 가졌고 그들은 나에게 명확하게 설명했습니다.AnyView
View보다 성능이 크게 저하되므로 예외적인 경우에만 사용해야 합니다. 는 의 AnyView
컴파일 중에는 해결할 수 없으므로 힙에 할당해야 합니다.
2022년 6월 갱신:
했습니다.NavigationStack
.
https://developer.apple.com/documentation/swiftui/navigationstack/
NavigationStack
에서는, 를 현재의 할 수 있습니다.이 뷰에서는, 「행선지 뷰」를 합니다..navigationDestination
코디네이터를 할 수 있게 드디어 깔끔한 코디네이터를 할 수 있는 방법이야
@Apple을 들어주셔서 감사합니다!
폐쇄만 하면 돼!
struct ItemsView<Destination: View>: View {
let items: [Item]
let buildDestination: (Item) -> Destination
var body: some View {
NavigationView {
List(items) { item in
NavigationLink(destination: self.buildDestination(item)) {
Text(item.id.uuidString)
}
}
}
}
}
나는 Swift에서 대리자 패턴 교체에 대한 글을 썼다.닫힘이 있는 UI.https://swiftwithmajid.com/2019/11/06/the-power-of-closures-in-swiftui/
'어느 정도'의이 될 것 같습니다.Coordinator
★★★★★★★★★★★★★★★★★」Delegate
'만들기'를 ,Coordinator
링크:
struct Coordinator {
let window: UIWindow
func start() {
var view = ContentView()
window.rootViewController = UIHostingController(rootView: view)
window.makeKeyAndVisible()
}
}
<고객명>을 SceneDelegate
Coordinator
:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let coordinator = Coordinator(window: window)
coordinator.start()
}
}
★★의 ContentView
하다
struct ContentView: View {
var delegate: ContentViewDelegate?
var body: some View {
NavigationView {
List {
NavigationLink(destination: delegate!.didSelect(Item())) {
Text("Destination1")
}
}
}
}
}
「 」를 할 수 .ContenViewDelegate
다음과 같은 프로토콜:
protocol ContentViewDelegate {
func didSelect(_ item: Item) -> AnyView
}
서 ★★★★★Item
단지 식별할 수 있는 구조일 뿐이며, 다른 모든 것이 될 수 있습니다(예: id of the elements in a first.TableView
( (영어) ()UIKit ★★)
는 이 입니다.Coordinator
돼요.
extension Coordinator: ContentViewDelegate {
func didSelect(_ item: Item) -> AnyView {
AnyView(Text("Returned Destination1"))
}
}
지금까지 제 앱에서 잘 작동했어요.도움이 됐으면 좋겠어요.
저는 당신의 요점에 대해 하나씩 답하도록 노력하겠습니다.할 수 할 .View
그것은 을 나타내고 있다Text
a. a. a.NavigationLink
에게도 갈 수 있는 그대로다.Destination
Gist: Swift를 만들었습니다.UI - 전체 예제를 보려면 코디네이터를 사용하여 유연한 탐색을 수행합니다.
설계상의 문제:NavigationLinks는 보기에 하드 코딩됩니다.
다른 할 수 .struct MyView<Destination: View>: View
에 적합한 할 수 있습니다 이제 View에 적합한 유형을 대상으로 사용할 수 있습니다.
그러나 이 NavigationLink를 포함하는 뷰를 재사용할 수 있어야 하는 경우 수신처를 하드코드로 지정할 수 없습니다.목적지를 제공하는 메커니즘이 있어야 합니다.
위의 변경으로 유형을 제공하는 메커니즘이 있습니다.예를 들어 다음과 같습니다.
struct BoldTextView: View {
var text: String
var body: some View {
Text(text)
.bold()
}
}
struct NotReusableTextView: View {
var text: String
var body: some View {
VStack {
Text(text)
NavigationLink("Link", destination: BoldTextView(text: text))
}
}
}
로 바뀝니다.
struct ReusableNavigationLinkTextView<Destination: View>: View {
var text: String
var destination: () -> Destination
var body: some View {
VStack {
Text(text)
NavigationLink("Link", destination: self.destination())
}
}
}
다음과 같이 목적지를 통과할 수 있습니다.
struct BoldNavigationLink: View {
let text = "Text"
var body: some View {
ReusableNavigationLinkTextView(
text: self.text,
destination: { BoldTextView(text: self.text) }
)
}
}
재사용 가능한 화면이 여러 개 있는 즉시 하나의 재사용 가능한 보기(ViewA)에 사전 구성된 보기 대상(ViewB)이 필요하다는 논리적 문제가 발생합니다.단, ViewB에도 사전 설정된 View 수신처 ViewC가 필요한 경우에는 어떻게 해야 합니까?ViewB를 ViewA에 삽입하기 전에 ViewB에 이미 ViewC가 삽입되도록 ViewB를 작성해야 합니다.기타 등등...
분명히 의 논리를 어떤 가 필요할 .Destination
뷰가 합니다 어떤 시점에서 뷰에 다음에 어떤 뷰가 표시되는지 알려줘야 합니다.하려는 건런 ??????
struct NestedMainView: View {
@State var text: String
var body: some View {
ReusableNavigationLinkTextView(
text: self.text,
destination: {
ReusableNavigationLinkTextView(
text: self.text,
destination: {
BoldTextView(text: self.text)
}
)
}
)
}
}
.Coordinator
s: 존존 s s s s s s s s s s s s s s s s.코디네이터를 위한 프로토콜이 있으며 이를 기반으로 특정 사용 사례를 구현할 수 있습니다.
protocol ReusableNavigationLinkTextViewCoordinator {
associatedtype Destination: View
var destination: () -> Destination { get }
func createView() -> ReusableNavigationLinkTextView<Destination>
}
으로, 「코디네이터」를 할 수 있게 .BoldTextView
를 했을 때NavigationLink
.
struct ReusableNavigationLinkShowBoldViewCoordinator: ReusableNavigationLinkTextViewCoordinator {
@Binding var text: String
var destination: () -> BoldTextView {
{ return BoldTextView(text: self.text) }
}
func createView() -> ReusableNavigationLinkTextView<Destination> {
return ReusableNavigationLinkTextView(text: self.text, destination: self.destination)
}
}
'아까', '아까', '아까', '아까'를하실 수 .Coordinator
뷰의 대상을 결정하는 커스텀 로직을 실장합니다.는 '하다'를 나타냅니다.ItalicTextView
4번
struct ItalicTextView: View {
var text: String
var body: some View {
Text(text)
.italic()
}
}
struct ShowNavigationLinkUntilNumberGreaterFourThenItalicViewCoordinator: ReusableNavigationLinkTextViewCoordinator {
@Binding var text: String
let number: Int
private var isNumberGreaterThan4: Bool {
return number > 4
}
var destination: () -> AnyView {
{
if self.isNumberGreaterThan4 {
let coordinator = ItalicTextViewCoordinator(text: self.text)
return AnyView(
coordinator.createView()
)
} else {
let coordinator = ShowNavigationLinkUntilNumberGreaterFourThenItalicViewCoordinator(
text: self.$text,
number: self.number + 1
)
return AnyView(coordinator.createView())
}
}
}
func createView() -> ReusableNavigationLinkTextView<AnyView> {
return ReusableNavigationLinkTextView(text: self.text, destination: self.destination)
}
}
전달해야 하는 데이터가 있는 경우 다른 코디네이터 주위에 값을 유지할 다른 코디네이터를 작성합니다.에서는, 「」라고 것이 있습니다.TextField
->EmptyView
->Text
을 TextField로 합니다.Text.
EmptyView
에는 이 정보가 없어야 합니다.
struct TextFieldView<Destination: View>: View {
@Binding var text: String
var destination: () -> Destination
var body: some View {
VStack {
TextField("Text", text: self.$text)
NavigationLink("Next", destination: self.destination())
}
}
}
struct EmptyNavigationLinkView<Destination: View>: View {
var destination: () -> Destination
var body: some View {
NavigationLink("Next", destination: self.destination())
}
}
다른 코디네이터를 호출하여 뷰를 작성하거나 뷰 자체를 작성하는 코디네이터입니다.다음 값을 전달합니다.TextField
로로 합니다.Text
및EmptyView
모르는 일이에요.
struct TextFieldEmptyReusableViewCoordinator {
@Binding var text: String
func createView() -> some View {
let reusableViewBoldCoordinator = ReusableNavigationLinkShowBoldViewCoordinator(text: self.$text)
let reusableView = reusableViewBoldCoordinator.createView()
let emptyView = EmptyNavigationLinkView(destination: { reusableView })
let textField = TextFieldView(text: self.$text, destination: { emptyView })
return textField
}
}
것을 하려면 , 「 」를 작성할 도 있습니다.MainView
은 있어서 무엇을 결정할 수 있다.View
Coordinator
사용해야 합니다.
struct MainView: View {
@State var text = "Main"
var body: some View {
NavigationView {
VStack(spacing: 32) {
NavigationLink("Bold", destination: self.reuseThenBoldChild())
NavigationLink("Reuse then Italic", destination: self.reuseThenItalicChild())
NavigationLink("Greater Four", destination: self.numberGreaterFourChild())
NavigationLink("Text Field", destination: self.textField())
}
}
}
func reuseThenBoldChild() -> some View {
let coordinator = ReusableNavigationLinkShowBoldViewCoordinator(text: self.$text)
return coordinator.createView()
}
func reuseThenItalicChild() -> some View {
let coordinator = ReusableNavigationLinkShowItalicViewCoordinator(text: self.$text)
return coordinator.createView()
}
func numberGreaterFourChild() -> some View {
let coordinator = ShowNavigationLinkUntilNumberGreaterFourThenItalicViewCoordinator(text: self.$text, number: 1)
return coordinator.createView()
}
func textField() -> some View {
let coordinator = TextFieldEmptyReusableViewCoordinator(text: self.$text)
return coordinator.createView()
}
}
는 '계산할 수 없다'는 '계산할 수 없다'도 만들 수 있다는 것을 알고 있습니다.Coordinator
프로토콜과 몇 가지 기본 메서드를 사용 방법에 대한 간단한 예를 보여드리고자 합니다.
이 은 제가 유사합니다.Coordinator
UIKit
communications applications.communications about.
질문이나 피드백, 개선해야 할 점이 있으면 알려주세요.
다음은 프로그래밍 방식으로 다음 세부 보기를 위해 데이터를 무한 드릴다운하고 변경하는 재미있는 예입니다.
import SwiftUI
struct ContentView: View {
@EnvironmentObject var navigationManager: NavigationManager
var body: some View {
NavigationView {
DynamicView(viewModel: ViewModel(message: "Get Information", type: .information))
}
}
}
struct DynamicView: View {
@EnvironmentObject var navigationManager: NavigationManager
let viewModel: ViewModel
var body: some View {
VStack {
if viewModel.type == .information {
InformationView(viewModel: viewModel)
}
if viewModel.type == .person {
PersonView(viewModel: viewModel)
}
if viewModel.type == .productDisplay {
ProductView(viewModel: viewModel)
}
if viewModel.type == .chart {
ChartView(viewModel: viewModel)
}
// If you want the DynamicView to be able to be other views, add to the type enum and then add a new if statement!
// Your Dynamic view can become "any view" based on the viewModel
// If you want to be able to navigate to a new chart UI component, make the chart view
}
}
}
struct InformationView: View {
@EnvironmentObject var navigationManager: NavigationManager
let viewModel: ViewModel
// Customize your view based on more properties you add to the viewModel
var body: some View {
VStack {
VStack {
Text(viewModel.message)
.foregroundColor(.white)
}
.frame(width: 300, height: 300)
.background(Color.blue)
NavigationLink(destination: navigationManager.destination(forModel: viewModel)) {
Text("Navigate")
}
}
}
}
struct PersonView: View {
@EnvironmentObject var navigationManager: NavigationManager
let viewModel: ViewModel
// Customize your view based on more properties you add to the viewModel
var body: some View {
VStack {
VStack {
Text(viewModel.message)
.foregroundColor(.white)
}
.frame(width: 300, height: 300)
.background(Color.red)
NavigationLink(destination: navigationManager.destination(forModel: viewModel)) {
Text("Navigate")
}
}
}
}
struct ProductView: View {
@EnvironmentObject var navigationManager: NavigationManager
let viewModel: ViewModel
// Customize your view based on more properties you add to the viewModel
var body: some View {
VStack {
VStack {
Text(viewModel.message)
.foregroundColor(.white)
}
.frame(width: 300, height: 300)
.background(Color.green)
NavigationLink(destination: navigationManager.destination(forModel: viewModel)) {
Text("Navigate")
}
}
}
}
struct ChartView: View {
@EnvironmentObject var navigationManager: NavigationManager
let viewModel: ViewModel
var body: some View {
VStack {
VStack {
Text(viewModel.message)
.foregroundColor(.white)
}
.frame(width: 300, height: 300)
.background(Color.green)
NavigationLink(destination: navigationManager.destination(forModel: viewModel)) {
Text("Navigate")
}
}
}
}
struct ViewModel {
let message: String
let type: DetailScreenType
}
enum DetailScreenType: String {
case information
case productDisplay
case person
case chart
}
class NavigationManager: ObservableObject {
func destination(forModel viewModel: ViewModel) -> DynamicView {
DynamicView(viewModel: generateViewModel(context: viewModel))
}
// This is where you generate your next viewModel dynamically.
// replace the switch statement logic inside with whatever logic you need.
// DYNAMICALLY MAKE THE VIEWMODEL AND YOU DYNAMICALLY MAKE THE VIEW
// You could even lead to a view with no navigation link in it, so that would be a dead end, if you wanted it.
// In my case my "context" is the previous viewMode, by you could make it something else.
func generateViewModel(context: ViewModel) -> ViewModel {
switch context.type {
case .information:
return ViewModel(message: "Serial Number 123", type: .productDisplay)
case .productDisplay:
return ViewModel(message: "Susan", type: .person)
case .person:
return ViewModel(message: "Get Information", type: .chart)
case .chart:
return ViewModel(message: "Chart goes here. If you don't want the navigation link on this page, you can remove it! Or do whatever you want! It's all dynamic. The point is, the DynamicView can be as dynamic as your model makes it.", type: .information)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(NavigationManager())
}
}
당신이 이렇게 말할 때 생각나는 게 있어요.
단, ViewB에도 사전 설정된 View 수신처 ViewC가 필요한 경우에는 어떻게 해야 합니까?ViewB를 ViewA에 삽입하기 전에 ViewB에 이미 ViewC가 삽입되도록 ViewB를 작성해야 합니다.기타... 그러나 그 때 전달해야 할 데이터를 사용할 수 없기 때문에 전체 구축이 실패합니다.
그렇지 않아.뷰를 제공하는 대신 필요에 따라 뷰를 제공하는 폐쇄를 제공하도록 재사용 가능한 구성요소를 설계할 수 있습니다.
이렇게 하면 ViewB를 온 디맨드로 생성하는 폐쇄에 ViewC를 생성하는 폐쇄를 제공할 수 있지만 뷰의 실제 구축은 필요한 컨텍스트 정보를 사용할 수 있는 시점에 이루어질 수 있습니다.
이것은 전혀 엉뚱한 대답이기 때문에, 아마 말도 안 되는 것으로 판명될지도 모르지만, 저는 하이브리드 어프로치를 사용하고 싶다고 생각하고 있습니다.
환경을 사용하여 단일 코디네이터 개체를 통과합니다. Navigation Coordinator라고 합니다.
재사용 가능한 뷰에 동적으로 설정된 일종의 식별자를 지정합니다.이 식별자는 클라이언트 응용 프로그램의 실제 사용 사례 및 탐색 계층에 해당하는 의미 정보를 제공합니다.
재사용 가능한 뷰가 Navigation Coordinator에 대상 뷰를 조회하도록 하고 해당 ID와 탐색 중인 뷰 유형의 ID를 전달합니다.
이렇게 하면 Navigation Coordinator는 단일 주입점으로 유지되며 뷰 계층 외부에서 액세스할 수 있는 뷰 이외의 객체입니다.
설정 중에 런타임에 전달된 식별자와 일치하여 올바른 뷰 클래스를 등록하여 반환할 수 있습니다.경우에 따라서는, 행선지 ID 와의 매칭과 같은 간단한 조작이 기능하는 경우가 있습니다.또는, 호스트 및 행선지 식별자 쌍에 대해서 대조합니다.
더 복잡한 경우에는 다른 앱별 정보를 고려하는 사용자 지정 컨트롤러를 작성할 수 있습니다.
이 뷰는 환경을 통해 주입되므로 임의의 시점에서 기본 Navigation Coordinator를 덮어쓰고 다른 Navigation Coordinator를 서브뷰에 제공할 수 있습니다.
, 「」를 하기 위해서입니다.NavigationLink
이에 대한 구체적인 견해를 제시해야 합니다.따라서 이 의존관계를 해소할 필요가 있는 경우에는 타입 삭제가 필요합니다. AnyView
다음은 엄격한 의존관계를 피하기 위해 유형 삭제 뷰를 사용하는 라우터/ViewModel 개념을 기반으로 한 아이디어의 데모를 보여 줍니다.Xcode 11.4/iOS 13.4로 테스트 완료.
우선, 취득한 정보의 종료부터 시작해 분석합니다(코멘트).
struct DemoContainerView: View {
var router: Router // some router
var vm: [RouteModel] // some view model having/being route model
var body: some View {
RouteContainer(router: router) { // route container with UI layout
List {
ForEach(self.vm.indices, id: \.self) {
Text("Label \($0)")
.routing(with: self.vm[$0]) // modifier giving UI element
// possibility to route somewhere
// depending on model
}
}
}
}
}
struct TestRouter_Previews: PreviewProvider {
static var previews: some View {
DemoContainerView(router: SimpleRouter(),
vm: (1...10).map { SimpleViewModel(text: "Item \($0)") })
}
}
따라서, 우리는 어떤 내비게이션의 세부 사항도 없는 순수한 UI와 이 UI가 어디로 라우팅될 수 있는지에 대한 분리된 지식을 가지고 있습니다.작동 방식은 다음과 같습니다.
구성 요소:
// Base protocol for route model
protocol RouteModel {}
// Base protocol for router
protocol Router {
func destination(for model: RouteModel) -> AnyView
}
// Route container wrapping NavigationView and injecting router
// into view hierarchy
struct RouteContainer<Content: View>: View {
let router: Router?
private let content: () -> Content
init(router: Router? = nil, @ViewBuilder _ content: @escaping () -> Content) {
self.content = content
self.router = router
}
var body: some View {
NavigationView {
content()
}.environment(\.router, router)
}
}
// Modifier making some view as routing element by injecting
// NavigationLink with destination received from router based
// on some model
struct RouteModifier: ViewModifier {
@Environment(\.router) var router
var rm: RouteModel
func body(content: Content) -> some View {
Group {
if router == nil {
content
} else {
NavigationLink(destination: router!.destination(for: rm)) { content }
}
}
}
}
// standard view extension to use RouteModifier
extension View {
func routing(with model: RouteModel) -> some View {
self.modifier(RouteModifier(rm: model))
}
}
// Helper environment key to inject Router into view hierarchy
struct RouterKey: EnvironmentKey {
static let defaultValue: Router? = nil
}
extension EnvironmentValues {
var router: Router? {
get { self[RouterKey.self] }
set { self[RouterKey.self] = newValue }
}
}
데모에 표시된 테스트 코드:
protocol SimpleRouteModel: RouteModel {
var next: AnyView { get }
}
class SimpleViewModel: ObservableObject {
@Published var text: String
init(text: String) {
self.text = text
}
}
extension SimpleViewModel: SimpleRouteModel {
var next: AnyView {
AnyView(DemoLevel1(rm: self))
}
}
class SimpleEditModel: ObservableObject {
@Published var vm: SimpleViewModel
init(vm: SimpleViewModel) {
self.vm = vm
}
}
extension SimpleEditModel: SimpleRouteModel {
var next: AnyView {
AnyView(DemoLevel2(em: self))
}
}
class SimpleRouter: Router {
func destination(for model: RouteModel) -> AnyView {
guard let simpleModel = model as? SimpleRouteModel else {
return AnyView(EmptyView())
}
return simpleModel.next
}
}
struct DemoLevel1: View {
@ObservedObject var rm: SimpleViewModel
var body: some View {
VStack {
Text("Details: \(rm.text)")
Text("Edit")
.routing(with: SimpleEditModel(vm: rm))
}
}
}
struct DemoLevel2: View {
@ObservedObject var em: SimpleEditModel
var body: some View {
HStack {
Text("Edit:")
TextField("New value", text: $em.vm.text)
}
}
}
struct DemoContainerView: View {
var router: Router
var vm: [RouteModel]
var body: some View {
RouteContainer(router: router) {
List {
ForEach(self.vm.indices, id: \.self) {
Text("Label \($0)")
.routing(with: self.vm[$0])
}
}
}
}
}
// MARK: - Preview
struct TestRouter_Previews: PreviewProvider {
static var previews: some View {
DemoContainerView(router: SimpleRouter(), vm: (1...10).map { SimpleViewModel(text: "Item \($0)") })
}
}
나는 나의 솔루션을 기사에 게재했다 - Routing in SwiftUI. Swift에서 라우팅하기 위한 두 가지 솔루션UI.
개요는 다음과 같습니다.
1. 트리거 뷰가 있는 라우터라우터는 가능한 모든 네비게이션루트에 대해 트리거 서브뷰를 반환하여 프레젠테이션뷰에 삽입합니다.이러한 서브뷰 코드 스니펫에는 NavigationLink 또는 .sheet 수식자 및 지정된 수신처 뷰가 포함되어 바인딩을 통해 라우터에 저장된 상태 속성이 사용됩니다.이렇게 하면 표시 보기는 네비게이션 코드와 대상에 의존하지 않고 라우터 프로토콜에만 의존합니다.
표시 예:
protocol PresentingRouterProtocol: NavigatingRouter {
func presentDetails<TV: View>(text: String, triggerView: @escaping () -> TV) -> AnyView
}
struct PresentingView<R: PresentingRouterProtocol>: View {
@StateObject private var router: R
init(router: R) {
_router = StateObject(wrappedValue: router)
}
var body: some View {
NavigationView {
router.presentDetails(text: "Details") {
Text("Present Details")
.padding()
}
}
}
}
라우터의 예를 다음에 나타냅니다.
class PresentingRouter: PresentingRouterProtocol {
struct NavigationState {
var presentingDetails = false
}
@Published var navigationState = NavigationState()
func presentDetails<TV: View>(text: String, triggerView: @escaping () -> TV) -> AnyView {
let destinationView = PresentedView(text: text, router: BasePresentedRouter(isPresented: binding(keyPath: \.presentingDetails)))
return AnyView(SheetButton(isPresenting: binding(keyPath: \.presentingDetails), contentView: triggerView, destinationView: destinationView))
}
}
SheetButton 트리거 보기:
struct SheetButton<CV: View, DV: View>: View {
@Binding var isPresenting: Bool
var contentView: () -> CV
var destinationView: DV
var body: some View {
Button(action: {
self.isPresenting = true
}) {
contentView()
.sheet(isPresented: $isPresenting) {
self.destinationView
}
}
}
}
소스 코드: https://github.com/ihorvovk/Routing-in-SwiftUI-with-trigger-views
(2) 타입이 지워진 수식어가 있는 라우터.프레젠테이션 뷰는 .navigation(라우터), .sheet(라우터) 등의 다른 뷰를 표시하는 일반적인 수식어로 구성됩니다.라우터에서 초기화되면 이러한 수식자는 바인딩을 통해 라우터에 저장되어 있는 내비게이션스테이트를 추적하고 라우터가 네비게이션스테이트를 변경했을 때 네비게이션을 실행합니다.라우터에는 가능한 모든 네비게이션 기능도 있습니다.이러한 기능은 상태를 변경하고 결과적으로 항법을 트리거합니다.
표시 예:
protocol PresentingRouterProtocol: Router {
func presentDetails(text: String)
}
struct PresentingView<R: PresentingRouterProtocol>: View {
@StateObject private var router: R
init(router: R) {
_router = StateObject(wrappedValue: router)
}
var body: some View {
NavigationView {
Button(action: {
router.presentDetails(text: "Details")
}) {
Text("Present Details")
.padding()
}.navigation(router)
}.sheet(router)
}
}
custome .sheet 수식자는 라우터를 파라미터로 사용합니다.
struct SheetModifier: ViewModifier {
@Binding var presentingView: AnyView?
func body(content: Content) -> some View {
content
.sheet(isPresented: Binding(
get: { self.presentingView != nil },
set: { if !$0 {
self.presentingView = nil
}})
) {
self.presentingView
}
}
}
기본 라우터 클래스:
class Router: ObservableObject {
struct State {
var navigating: AnyView? = nil
var presentingSheet: AnyView? = nil
var isPresented: Binding<Bool>
}
@Published private(set) var state: State
init(isPresented: Binding<Bool>) {
state = State(isPresented: isPresented)
}
}
서브클래스는 사용 가능한 루트에 대해서만 기능을 구현해야 합니다.
class PresentingRouter: Router, PresentingRouterProtocol {
func presentDetails(text: String) {
let router = Router(isPresented: isNavigating)
navigateTo (
PresentedView(text: text, router: router)
)
}
}
소스 코드: https://github.com/ihorvovk/Routing-in-SwiftUI-with-type-erased-modifiers
두 솔루션 모두 네비게이션 로직을 뷰 레이어에서 분리합니다.둘 다 네비게이션스테이트를 라우터에 저장합니다.라우터의 상태를 변경하는 것만으로 네비게이션을 실행하고 딥링크를 구현할 수 있습니다.
iOS 16 이상
iOS 16에서는 NavigationStack 및 NavigationPath에 액세스할 수 있습니다.
다음은 매우 간단한 데모입니다.
- 하다, 하다, 하다, 하다, 하다, 하다, 하다, 이런 을 만들 수 있어요.
NavigationPath
조작할 수 있습니다.
class Coordinator: ObservableObject {
@Published var path = NavigationPath()
func show<V>(_ viewType: V.Type) where V: View {
path.append(String(describing: viewType.self))
}
func popToRoot() {
path.removeLast(path.count)
}
}
- 아, 아, 아, 아, 아, 아, 아, 아, 아, 아, 아, 아, 아, 아, 아, 아, 아, 아, 아, 아, 아, 아,
RootView
.NavigationStack
,, 은, 은, 델, 델, 델, ., ., ..navigationDestination
이치
struct RootView: View {
@StateObject private var coordinator = Coordinator()
var body: some View {
NavigationStack(path: $coordinator.path) {
VStack {
Button {
coordinator.show(ViewA.self)
} label: {
Text("Show View A")
}
Button {
coordinator.show(ViewB.self)
} label: {
Text("Show View B")
}
}
.navigationDestination(for: String.self) { id in
if id == String(describing: ViewA.self) {
ViewA()
} else if id == String(describing: ViewB.self) {
ViewB()
}
}
}
.environmentObject(coordinator)
}
}
- 의 모든 에는 """만 합니다.
Coordinator
오브젝트 및 하드코드된 라우팅 제어는 없습니다.
struct ViewA: View {
@EnvironmentObject private var coordinator: Coordinator
var body: some View {
VStack {
Text("This is View A")
Button {
coordinator.popToRoot()
} label: {
Text("Go to root")
}
}
}
}
struct ViewB: View {
@EnvironmentObject private var coordinator: Coordinator
var body: some View {
VStack {
Text("This is View B")
Button {
coordinator.show(ViewA.self)
} label: {
Text("Show View A")
}
}
}
}
정말 흥미로운 주제네요 여러분.여기에 내 돈을 넣기 위해, 나는 내 생각을 공유할 것이다.나는 그 문제에 대해 너무 많은 의견을 내지 않고 주로 집중하려고 노력했다.
예를 들어, 사내에서 전 세계에 출하할 필요가 있는 UI 컴포넌트 프레임워크를 구축하고 있다고 합시다.그 후 필요한 것은 '더미' 컴포넌트를 만드는 것입니다.이 컴포넌트는 자신을 나타내는 방법과 내비게이션이 있는지 여부 등 최소한의 지식만을 갖추고 있습니다.
전제 조건:
- ViewA 컴포넌트는 UI로 분리된 프레임워크에 존재합니다.
- View A 컴포넌트는 이 컴포넌트로부터 네비게이트 할 수 있는 것을 알 수 있습니다.그러나 ViewA는 그 안에 무엇이 살고 있는지에 대해 크게 신경쓰지 않습니다."잠재적으로" 탐색 가능한 뷰만 제공하면 됩니다.따라서, 성립하는 「계약」은 다음과 같습니다.고차 컴포넌트 삭제 타입 빌더(React에서 영감을 받아 iOS:D에서 오랜 세월 동안 작업한 후 알려드립니다)는 컴포넌트에서 뷰를 받게 됩니다.그리고 이 빌더는 뷰를 제공합니다.바로 그겁니다.View A는 다른 것을 알 필요가 없습니다.
표시 A
/// UI Library Components framework.
struct ViewAPresentable: Identifiable {
let id = UUID()
let text1: String
let text2: String
let productLinkTitle: String
}
struct ViewA: View {
let presentable: ViewAPresentable
let withNavigationBuilder: (_ innerView: AnyView) -> AnyView
var body: some View {
VStack(alignment: .leading,
spacing: 10) {
HStack(alignment: .firstTextBaseline,
spacing: 8) {
Text(presentable.text1)
Text(presentable.text2)
}
withNavigationBuilder(AnyView(Text(presentable.productLinkTitle)))
}
}
}
그럼.
- 이 컴포넌트를 소비하는 HostA가 있으며 실제로 HOC에 네비게이션 가능한 링크를 제공하려고 합니다.
/// HOST A: Consumer of that component.
struct ConsumerView: View {
let presentables: [ViewAPresentable] = (0...10).map {
ViewAPresentable(text1: "Hello",
text2: "I'm \($0)",
productLinkTitle: "Go to product")
}
var body: some View {
NavigationView {
List(presentables) {
ViewA(presentable: $0) { innerView in
AnyView(NavigationLink(destination: ConsumerView()) {
innerView
})
}
}
}
}
}
하지만 실제로는 또 다른 소비자 B입니다.네비게이션 가능한 링크를 제공하려는 것이 아니라 컨슈머 B에서 네비게이션을 할 수 없다는 요건이 주어지기 때문에 내부 컴포넌트만 제공합니다.
/// HOST B: Consumer of that component. (But here it's not navigatable)
struct ConsumerBView: View {
let presentables: [ViewAPresentable] = (0...10).map {
ViewAPresentable(text1: "Hello",
text2: "I'm \($0)",
productLinkTitle: "Product description not available")
}
var body: some View {
NavigationView {
List(presentables) {
ViewA(presentable: $0) { innerView in
AnyView(innerView)
}
}
}
}
}
위의 코드를 확인함으로써 최소한의 계약이 성립된 격리된 컴포넌트를 얻을 수 있습니다.여기서 type erasure는 컨텍스트에서 암묵적으로 요구되기 때문에 type erasure로 넘어갔습니다.View A는 실제로 그 안에 무엇을 배치할지는 신경 쓰지 않습니다.소비자의 책임입니다.
이를 바탕으로 Factory Builders, Coordinators 등과 함께 솔루션을 추상화할 수 있습니다.하지만 실제로 문제의 근원은 해결되었습니다.
나도 그 문제에 도전하기로 했다.
환경을 통한 의존성 주입이 보다 깨끗한 접근법이라고 쉽게 주장할 수 있고, 실제로도 그럴 수 있지만, 목적지의 결정 장소에서의 컨텍스트 정보로서 범용 데이터 타입을 사용할 수 없기 때문에 나는 반대하기로 결정했다.즉, 제네릭스를 사전에 전문화하지 않고는 환경에 투입할 수 없습니다.
대신 사용하기로 결정한 패턴은 다음과 같습니다.
프레임워크측
Segue 조정 프로토콜
의 프로토콜입니다.Segueing
.
protocol Segueing {
associatedtype Destination: View
associatedtype Segue
func destination(for segue: Segue) -> Destination
}
뷰에 연결된 모든 sego 코디네이터가 콘크리트 sego에 응답하여 목적지로 다른 뷰를 제공할 수 있어야 한다는 계약을 정의합니다.
segue가 열거일 필요는 없지만 목적에 필요한 컨텍스트를 전달하기 위해 관련 유형으로 증강된 유한 열거를 사용하는 것이 실용적입니다.
Segue 열거
enum Destinations<Value> {
case details(_ context: Value)
}
다음 예제에서는 단일 segue "상세"를 정의하고 임의의 유형 Value를 사용하여 사용자 선택의 컨텍스트를 안전한 방식으로 전달하는 방법을 보여 줍니다.긴밀하게 함께 작동하는 뷰 그룹에 대해 단일 segue 열거를 사용할지 또는 각 뷰가 자체 보기를 정의하도록 할지는 설계 선택 사항입니다.각 보기가 고유한 일반 유형을 가져올 경우 후자가 더 선호됩니다.
보다
struct ListView<N: Segueing, Value>: View where N.Segue == Destinations<Value>, Value: CustomStringConvertible & Hashable {
var segues: N
var items: [Value]
var body: some View {
NavigationView {
List(items, id: \.self) { item in
NavigationLink(destination: self.segues.destination(for: .details(item))) {
Text("\(item.description)")
}
}
}
}
}
과 같습니다.Value
볼 수 있습니다. 우리는 또한 segue 코디네이터와 segue 코디네이터 사이의 관계를 확립합니다.N: Segueing
segue ( segue enumeration)Destinations
이할 수 segue 코디네이터는 segue에서 사용 가능한 응답합니다.이 코디네이터는 에서 사용 가능한 세그먼트를 기반으로 합니다.Destinations
사용자가 선택한 값을 코디네이터에게 전달하여 의사결정을 합니다.
다음과 같이 보기를 조건부로 확장하고 새로운 편의 이니셜라이저를 도입하여 기본 segue 코디네이터를 정의할 수 있습니다.
extension ListView where N == ListViewSegues<Value> {
init(items: [Value]) {
self = ListView(segues: ListViewSegues(), items: items)
}
}
이는 모두 프레임워크 또는 스위프트 패키지 내에서 정의됩니다.
클라이언트측
세그 코디네이터
struct ListViewSegues<Value>: Segueing where Value: CustomStringConvertible {
func destination(for segue: Destinations<Value>) -> some View {
switch segue {
case .details(let value):
return DetailView(segues: DetailViewSegues(), value: value)
}
}
}
struct DetailViewSegues<Value>: Segueing where Value: CustomStringConvertible {
func destination(for segue: Destinations<Value>) -> some View {
guard case let .details(value) = segue else { return AnyView(EmptyView()) }
return AnyView(Text("Final destination: \(value.description)")
.foregroundColor(.white)
.padding()
.background(Capsule()
.foregroundColor(.gray))
)
}
}
세가를 볼 수 . 즉, 를 틀에서 를 예로 들 수 있습니다.DetailView
다른 segue 코디네이터를 제공하여 (사용자가 선택한) 값을 상세 뷰에 전달합니다.
콜 사이트에서
var v1 = ListView(segues: ListViewSegues(), items: [7, 5, 12])
var v2 = ListView(segues: ListViewSegues(), items: ["New York", "Tokyo", "Paris"])
var v3 = ListView(items: ["New York", "Tokyo", "Paris"])
혜택들
- 뷰를 재사용하여 프레임워크나 스위프트 패키지와 같은 개별 모듈로 분류할 수 있습니다.
- 네비게이션의 수신처는 클라이언트측에서 커스터마이즈 할 수 있기 때문에, 사전에 설정할 필요는 없습니다.
- 뷰 건설 현장에서 강력(콘텍스트) 유형 정보를 사용할 수 있습니다.
- 심층 뷰 계층 구조에서는 내포된 폐쇄가 발생하지 않습니다.
다음은 라우터를 사용하여 보기와 대상 보기를 분리하는 또 다른 제안 솔루션입니다.표시된 보기 유형과 프리젠테이션 스타일은 프리젠테이션 보기에서 멀리 떨어져 추상화되어 있습니다.
아래 첨부된 솔루션이나 샘플코드에 아키텍처상의 결함이 있다고 생각되면 알려주세요.
라우터:
import SwiftUI
protocol DetailsFeatureRouting {
func makePushDetailsView<Label: View>(viewModel: GrapeViewModel, @ViewBuilder label: () -> Label) -> AnyView
func makeModalDetailsView<Label: View>(viewModel: GrapeViewModel, @ViewBuilder label: () -> Label) -> AnyView
}
extension DetailsFeatureRouting {
func makePushDetailsView<Label: View>(viewModel: GrapeViewModel, @ViewBuilder label: () -> Label) -> AnyView {
label()
.makeNavigation {
DetailsView.make(viewModel: viewModel)
}
.anyView
}
func makeModalDetailsView<Label: View>(viewModel: GrapeViewModel, @ViewBuilder label: () -> Label) -> AnyView {
label()
.makeSheet {
NavigationView {
DetailsView.make(viewModel: viewModel)
}
}
.anyView
}
}
루트뷰
struct RootView: View {
@StateObject var presenter: RootPresenter
var body: some View {
NavigationView {
List {
ForEach(presenter.viewModels) { viewModel in
presenter.makeDestinationView(viewModel: viewModel) {
VStack(alignment: .leading) {
Text(viewModel.title)
.font(.system(size: 20))
.foregroundColor(.primary)
.lineLimit(3)
Text(viewModel.subtitle)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
.navigationTitle("Grapes")
}
}
}
프로젝트 전체는 https://github.com/nikolsky2/FeatureRoutingSwiftUI에 있습니다.
1년 전 일이지만, 이것은 흥미롭고 여전히 실제적인 질문이다.IMHO, 우리는 여전히 일반적인 문제에 대한 좋은 해결책과 베스트 프랙티스를 발견할 필요가 있습니다.
그러나 UIKIt의 코디네이터 패턴은 해결하려고 하는 문제에 대한 좋은 해결책이라고 생각하지 않습니다.또한 올바른 어플리케이션은 많은 문제를 야기하고 이를 아키텍처의 나머지 부분과 통합하는 방법에 대한 많은 의문점을 남깁니다.
인스위프트UI는 모두 정적이고 "사전 정의"되어 있기 때문에 어려움을 겪고 있기 때문에 역동성을 얻을 수 있는 방법을 찾아야 합니다.그래서 Swift에서도 같은 문제가 여전히 존재합니다.UI.
다음 접근방식은 네비게이션의 세 가지 측면 중 두 가지 측면(작성, 전환 및 구성)을 분리하고 전환 측면(IMHO)을 소스 뷰에 남깁니다.
다른 두 가지 측면 작성(대상 보기 및 구성)은 보기 계층에 있는 원본 보기의 상위 보기인 전용 "조정자" 보기에서 수행됩니다.
주의: SwiftUI 보기는 UIit에서와 같은 보기가 아닙니다.이는 이면에 존재하며 Swift에 의해 관리될 "View"를 만들고 수정하는 수단일 뿐이다.UI. 따라서 설정 및 구성만 수행하는 뷰를 사용하는 것은 IMHO가 전체적으로 유효하고 유용한 접근법입니다.적절한 이름과 규칙을 사용하면 이러한 보기를 식별할 수 있습니다.
해결책은 꽤 가벼운 무게입니다.특정 측면을 더 분리할 필요가 있는 경우, 예를 들어 목적지의 뷰를 요소뿐만 아니라 일부 환경에서 어떤 속성으로부터도 종속시킬 필요가 있는 경우, 저는 UIKit용으로 개발된 것과 같은 코디네이터 패턴에 의존하지 않습니다.인스위프트UI에는 더 나은 대안이 있습니다.애플리케이션이나 설정을 분해해, 한쪽과 다른 한쪽을 실장하는 「멀리」의 장소를 2개 가질 수 있도록 하는 「Reader Monad」등의 일반적인 기술을 사용하고 있습니다.이것은 기본적으로 의존성 주입의 한 형태입니다.
이 시나리오에서는 다음과 같습니다.
- 우리는 요소를 보여주는 목록 보기를 가지고 있다.
- 각 요소는 탐색 링크를 통해 상세 보기로 표시할 수 있습니다.
- 상세 뷰의 종류는 요소의 특정 특성에 따라 달라집니다.
import SwiftUI
import Combine
struct MasterView: View {
struct Selection: Identifiable {
let id: MasterViewModel.Item.ID
let view: () -> DetailCoordinatorView // AnyView, if you
// need strong decoupling
}
let items: [MasterViewModel.Item]
let selection: Selection?
let selectDetail: (_ id: MasterViewModel.Item.ID) -> Void
let unselectDetail: () -> Void
func link() -> Binding<MasterViewModel.Item.ID?> {
Binding {
self.selection?.id
} set: { id in
print("link: \(String(describing: id))")
if let id = id {
selectDetail(id)
} else {
unselectDetail()
}
}
}
var body: some View {
List {
ForEach(items, id: \.id) { element in
NavigationLink(
tag: element.id,
selection: link()) {
if let selection = self.selection {
selection.view()
}
} label: {
Text("\(element.name)")
}
}
}
}
}
마스터 보기에는 상세 보기에 대한 지식이 없습니다.하나의 네비게이션 링크만 사용하여 다양한 종류의 상세 뷰를 효과적으로 표시합니다.또한 상세 뷰의 종류를 결정하는 메커니즘도 알지 못합니다.다만, 이행의 종류를 알고 판단합니다.
struct DetailView: View {
let item: DetailViewModel.Item
var body: some View {
HStack {
Text("\(item.id)")
Text("\(item.name)")
Text("\(item.description)")
}
}
}
데모를 위한 상세 뷰입니다.
struct MasterCoordinatorView: View {
@ObservedObject private(set) var viewModel: MasterViewModel
var body: some View {
MasterView(
items: viewModel.viewState.items,
selection: detailSelection(),
selectDetail: viewModel.selectDetail(id:),
unselectDetail: viewModel.unselectDetail)
}
func detailSelection() -> MasterView.Selection? {
let detailSelection: MasterView.Selection?
if let selection = viewModel.viewState.selection {
detailSelection = MasterView.Selection(
id: selection.id,
view: {
// 1. Decision point where one can create
// different kind of views depending on
// the given element.
DetailCoordinatorView(viewModel: selection.viewModel)
//.eraseToAnyView() // if you need
// more decoupling
}
)
} else {
detailSelection = nil
}
return detailSelection
}
}
MasterCoordinatorView는 내비게이션의 메커니즘을 설정하고 ViewModel을 View에서 분리합니다.
struct DetailCoordinatorView: View {
@ObservedObject private(set) var viewModel: DetailViewModel
var body: some View {
// 2. Decision point where one can create different kind
// of views depending on the given element, using a switch
// statement for example.
switch viewModel.viewState.item.id {
case 1:
DetailView(item: viewModel.viewState.item)
.background(.yellow)
case 2:
DetailView(item: viewModel.viewState.item)
.background(.blue)
case 3:
DetailView(item: viewModel.viewState.item)
.background(.green)
default:
DetailView(item: viewModel.viewState.item)
.background(.red)
}
}
}
여기서 Detail Coordinator View는 상세 뷰를 선택합니다.
마지막으로 모델 보기:
final class MasterViewModel: ObservableObject {
struct ViewState {
var items: [Item] = []
var selection: Selection? = nil
}
struct Item: Identifiable {
var id: Int
var name: String
}
struct Selection: Identifiable {
var id: Item.ID
var viewModel: DetailViewModel
}
@Published private(set) var viewState: ViewState
init(items: [Item]) {
self.viewState = .init(items: items, selection: nil)
}
func selectDetail(id: Item.ID) {
guard let item = viewState.items.first(where: { id == $0.id } ) else {
return
}
let detailViewModel = DetailViewModel(
item: .init(id: item.id,
name: item.name,
description: "description of \(item.name)",
image: URL(string: "a")!)
)
self.viewState.selection = Selection(
id: item.id,
viewModel: detailViewModel)
}
func unselectDetail() {
self.viewState.selection = nil
}
}
final class DetailViewModel: ObservableObject {
struct Item: Identifiable, Equatable {
var id: Int
var name: String
var description: String
var image: URL
}
struct ViewState {
var item: Item
}
@Published private(set) var viewState: ViewState
init(item: Item) {
self.viewState = .init(item: item)
}
}
놀이터의 경우:
struct ContentView: View {
@StateObject var viewModel = MasterViewModel(items: [
.init(id: 1, name: "John"),
.init(id: 2, name: "Bob"),
.init(id: 3, name: "Mary"),
])
var body: some View {
NavigationView {
MasterCoordinatorView(viewModel: viewModel)
}
.navigationViewStyle(.stack)
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(ContentView())
extension View {
func eraseToAnyView() -> AnyView {
AnyView(self)
}
}
저는 Navigator의 저자입니다. Navigator의 도서관은View
NavigationLink
탐색 대상을 할 수 런타임에 모든 네비게이션 대상을 호출할 수 있습니다. 데스티네이션 가 없습니다.
으로 위임 오브젝트를 .navigator
에서 건져내다View
할 수 .
navigate(to:)
a pushes 에 푸시합니다.NavigationView
pop
the the the the the the the the the the the theView
the offNavigationView
popToRoot()
NavigationView
을View
기본 Swift 사용UI 네비게이션 패러다임(Navigation Link), 커스텀 네비게이션 또는 래퍼 뷰 없음
또한 내비게이션 스택을 추적하고 맞춤형 내비게이션 로직을 사용할 수 있습니다.여기 토막이 있습니다.
struct DetailScreen: ScreenView {
@EnvironmentObject var navigator: Navigator<ScreenID, MyViewFactory>
@State var showNextScreen: Bool = false
var screenId: ScreenID
var body: some View {
VStack(spacing: 32) {
Button("Next") {
navigator.navigate(to: calculateNextScreen())
}
.tint(.blue)
Button("Dismiss") {
navigator.pop()
}
.tint(.red)
}
.navigationTitle("Detail Screen")
.bindNavigation(self, binding: $showNextScreen)
}
}
언급URL : https://stackoverflow.com/questions/61304700/swiftui-how-to-avoid-navigation-hardcoded-into-the-view
'source' 카테고리의 다른 글
행 및 열 인덱스로 WPF 그리드의 제어에 프로그래밍 방식으로 액세스하는 방법은 무엇입니까? (0) | 2023.04.19 |
---|---|
커밋 메시지로 Git 저장소를 검색하는 방법 (0) | 2023.04.19 |
기본 스타일에서 스타일 상속 (0) | 2023.04.19 |
XAML에서 간단한 하이퍼링크를 만드는 방법 (0) | 2023.04.19 |
NSAray를 통해 어떻게 반복해야 하나요? (0) | 2023.04.19 |