본문으로 바로가기
728x90

1. 결과물 미리보기

2. 프로젝트 구조

  • View: View를 상속받은 클래스
    • FrameworkListView
      • FrameworkCell
    • FrameworkDetailView
  • ViewModel: ObservableObject를 상속받은 클래스
    • FrameworkListViewModel
    • FrameworkDetailViewModel
  • Data
    • FrameworkData
  • ViewController: UIViewControllerRepresentable을 상속받은 클래스
    • SafariView

3. 세부 구현

3-1. Data 파악(FrameworkData.swift)

AppleFramework 구조체는 Hashable과 Identifiable을 상속받는다.

이는 추후 구현할 ForEach문의 구현과 protocol을 구현하기 위함이다.

3-2. 메인 View 구현(FrameworkListView.swift, FrameworkDetailView.swift)

기본적인 View클래스의 구조는 아래와 같다.

PreviewProvider를 통해 미리보기가 가능하다.

import SwiftUI

struct MyListView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
    	// Draw View
        ...View {
        	ForEach(viewModel.item) {
            	...
            }
        }
    }
}

struct MyListView_Previews: PreviewProvider {
    static var previews: some View {
    	MyListView()
    }
}

NavigationView 및 NavigationLink를 구현하는 방법은 아래와 같다.

struct FrameworkListView: View {
    @StateObject var vm = FrameworkListViewModel()	// 화면에 뿌려줄 데이터
    
    // 레이아웃 구조(4열)
    let layout: [GridItem] = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible()),
    ]
    
    var body: some View {
        NavigationView {
            ScrollView {
                LazyVGrid(columns: layout) {
                    ForEach($vm.models) { $item in
                    	// 내부 Cell 생성
                        FrameworkCell(framework: $item)
                            .onTapGesture {
                                vm.isShowingDetail = true
                                vm.selectedItem = item
                            }
                    }
                    
                    // NavigationLink로 내부 View 표출
                    // 선택된 item으로 ViewModel을 구성하고, View를 만든다.
                    let des = FrameworkDetailView(viewModel: FrameworkDetailViewModel(framework: vm.selectedItem!))
                    NavigationLink(destination: des, isActive: $vm.isShowingDetail, label: { EmptyView() })
                }
                .padding([.top, .leading, .trailing], 16.0)
            }
            .navigationTitle("☀️ Apple Framework")
        }
    }
}

아래는 NavigationLink를 통해 호출된 항목(item)을 구성하는 View이다.

struct FrameworkDetailView: View {
    @StateObject var viewModel: FrameworkDetailViewModel
    
    var body: some View {
        VStack(spacing: 30) {
            Spacer()
            Image(viewModel.framework.imageName)
                .resizable()
                .frame(width: 90, height: 90)
            Text(viewModel.framework.name)
                .font(.system(size: 24, weight: .bold))
            Text(viewModel.framework.description)
                .font(.system(size: 16, weight: .regular))
            
            Spacer()
            
            Button {
                viewModel.isSafariPresented = true
            } label: {
                Text("Learn More")
                    .font(.system(size: 20, weight: .bold, design: .default))
                    .foregroundColor(.white)
            }
            .frame(maxWidth: .infinity, minHeight: 80)
            .background(.pink)
            .cornerRadius(40)
        }
        .padding(EdgeInsets(top: 0, leading: 30, bottom: 0, trailing: 30))
        .sheet(isPresented: $viewModel.isSafariPresented) {
            let url = URL(string: viewModel.framework.urlString)!
            SafariView(url: url)
        }
    }
}

3-3. 세부 View 구현(FrameworkCell.swift, SafariView.swift)

첫 화면에서 GridView를 구성하는 아이콘을 표기하는 방법은 아래와 같다.

struct FrameworkCell: View {
    @Binding var framework: AppleFramework
    
    var body: some View {
        VStack {
            Image(framework.imageName)
                .resizable()
                .aspectRatio(contentMode: .fit)
            Spacer()
            Text(framework.name)
                .font(.system(size: 16, weight: .bold))
                .foregroundColor(Color.blue)
                .multilineTextAlignment(.center)
            Spacer()
        }
    }
}

항목 선택시 나타나는 화면에서 Learn more 버튼 클릭시 나타나는 SafariView는 아래와 같다.

이는 View를 상속받지 않고 UIViewControllerRepresentable을 상속받는다.

struct SafariView: UIViewControllerRepresentable {
    let url: URL
    
    func makeUIViewController(context:
        UIViewControllerRepresentableContext<SafariView>) ->
        SFSafariViewController {
        SFSafariViewController(url: url)
    }
    
    func updateUIViewController(_ uiViewController:
        SFSafariViewController, context:
        UIViewControllerRepresentableContext<SafariView>) {}
}

3-4. ViewModel로 View와 Data 연결(FrameworkListViewModel.swift, FrameworkDetailViewModel.swift)

위의 코드에서도 볼 수 있듯이 @StateObject와 @Published 태그(Tag)로 Single source of truth 상태 관리를 할 수 있다.

그리고 이는 ObservableObject를 상속받은 클래스를 대상으로 한다.

신뢰성있게 변수와 데이터를 관리하는 방법이라고 생각하면 된다.

final class FrameworkListViewModel: ObservableObject {
    @Published var models: [AppleFramework] = AppleFramework.list
    @Published var isShowingDetail: Bool = false {
        didSet {
            print("isShowingDetail \(isShowingDetail)")
        }
    }
    @Published var selectedItem: AppleFramework?
}
final class FrameworkDetailViewModel: ObservableObject {
    @Published var framework: AppleFramework
    @Published var isSafariPresented: Bool = false
    
    init(framework: AppleFramework) {
        self.framework = framework
    }
}

4. 결론

SwiftUI는 HTML구조와 비슷해 직관적이면서도, 객체지향 개발을 적절히 가미해서 만들어진 것 같다.

MVVM 패턴을 잘 적용하도록하자!

728x90