2021년 1월 1일 금요일

내 iOS 앱 생성 프로세스 — 4 부

 

Unsplash에 Tirza van Dijk의 사진

소개

먼저 SwiftUI를 좋아한다고 말씀 드리겠습니다! 내가 SwiftUI의 팬인 이유는 무엇입니까? 명령 적 (인터페이스 빌더) 접근 방식보다 인터페이스 개발에 대한 선언적 접근 방식을 선호합니다. SwiftUI를 사용하면 iOS 시뮬레이터를 실행하지 않고도 디자인 아이디어를 더 빠르게 반복하고 결과를 볼 수 있습니다. 내 팬보이 선언을 중단하고 SwiftUI 개발 프로세스를 살펴 보겠습니다.

참고 : 이것은 SwiftUI 튜토리얼이나 SwiftUI에 대한 자세한 소개가 아닙니다. developer.apple.com 및 raywenderlich.com 에는 이러한 정보에 대한 유용한 리소스가 많이 있습니다 . 내 기사와 코드 예제에서는 SwiftUI, SwiftUI 데이터 흐름 / 상태 관리 및 MVVM (Model-View-ViewModel) 디자인 패턴에 대한 몇 가지 기본 지식을 가정합니다.

어떻게 시작합니까?

내 시리즈 Part 3의 사용자 경험 (UX) 디자인과 와이어 프레임을 기억하십니까? 와이어 프레임은 나의 출발점 역할을합니다. 저의 기본적인 사용자 인터페이스 개발 철학은 각 와이어 프레임이 화면을 구성하는 하위 섹션 / 구성 요소 인 "보기"로 구성된 SwiftUI "화면"이라는 것입니다. 와이어 프레임의 SwiftUI 구현을 빌드 한 다음 화면을 구성하는 뷰로 필요에 따라 화면을 리팩터링하여 코드를 더 잘 정리했습니다.

또한 필요한 SwiftUI 코드를 개발하는 데 필요한 ViewModel 및 JSON 데이터 모델의 골격을 만듭니다. 예를 들어 앱은 Flickr의 사진 스트림을 표시하므로 Flickr 포토 스트림에서 사진을 디코딩하는 데 사용할 JSON 사진 모델이 필요할 것으로 예상됩니다. 또한 Flickr의 JSON 사진 스트림을 기대합니다. [Photos]뷰 모델의 photos라는 Swift 배열 은 Flickr의 사진 스트림을 나타냅니다. 다시 말하지만, SwiftUI를 사용하여 사용자 인터페이스를 구축하기 위해 이러한 모델과 뷰 모델을 완전히 구현할 필요는 없습니다. 모델과 뷰 모델 코드를 완성하는 것은 필요한 SwiftUI 인터페이스 코드를 작성한 후에 이루어집니다.

와이어 프레임 기반 화면 분해

와이어 프레임 기반 화면 분해 (Jody P. Abney)

MainScreen.swift다양한 구성 요소보기를 살펴 보겠습니다 . 기본 화면은 사용자가 사진, 즐겨 찾기 및 설정간에 전환 할 수있는 탭보기 탐색을 사용합니다. 첫 번째 화면 (사진)에 초점을 맞춘 다음은 화면을 구성하는보기의 개요입니다.

  • SearchBar.swift — 이보기는 흥미로운 사진, 최근 사진 및 주변 사진과 같이 앱에 대해 계획된 다양한 사진 스트림에서 Flickr 사진을 검색하는 표준 iOS 검색 막대를 생성합니다.
  • PhotoCategoryPicker.swift — 이보기는 사용자가 다양한 사진 스트림 (관심, 최근 및 주변) 중에서 선택할 수 있도록 세그먼트 화 된 컨트롤러보기를 만듭니다.
  • PhotoGrid.swift — 이보기 LazyZGrid는 선택한 포토 스트림에서 스크롤 가능한 사진을 만듭니다 .
  • PhotoGridCell.swift — 이보기는 선택한 포토 스트림 내 특정 이미지의 썸네일을 표시합니다. PhotoGridCell을 사용하면 사용자가지도보기 (사진에 지리적 위치 데이터를 사용할 수있는 경우)와 함께 사진의 더 큰보기와 제목, 사진 작가의 화면 이름, 사진 날짜와 같은 사진 세부 정보를 보여주는 PhotoScreen으로 이동할 수 있습니다. 몇 가지 데이터 요소의 이름을 지정하십시오. (다시 말하지만 이러한 데이터 요소는 JSON 사진 모델의 일부일 가능성이 높습니다.)

다음은 위의 접근 방식을 사용하여 만든 SwiftUI 화면의 스크린 샷입니다.

스크린 샷 : MainScreen (Jody P. Abney)

의 SwiftUI 코드 MainScreen.swift는 다음과 같습니다.

//
// MainScreen.swift
// MediumArticleFlickrApp
//
// Created by Jody Abney on 12/6/20.
//
import SwiftUI
struct MainScreen: View {
// Only minimal details of ViewModel are needed to declare
// the initial interface using SwiftUI
@ObservedObject var viewModel: ViewModel
var body: some View {
TabView {
// Flickr Photos
NavigationView {
VStack {
SearchBar(viewModel: viewModel)
PhotoCategoryPicker(viewModel: viewModel)
.padding([.leading, .trailing], 10)
if viewModel.photos.count == 0 {
Spacer()
EmptySection()
Spacer()
} else {
PhotoGrid(viewModel: viewModel)
}
}
.navigationTitle("Flickr Photos")
}
.tabItem {
VStack {
Image(systemName: "photo.on.rectangle")
Text("Flickr Photos")
}
}
// Manage Favorites
NavigationView {
VStack {
if viewModel.favPhotos.count == 0 {
Spacer()
EmptySection()
Spacer()
} else {
SearchBar(viewModel: viewModel)
FavoritesList(viewModel: viewModel)
}
}
.navigationTitle("Manage Favorites")
}
.tabItem {
VStack {
Image(systemName: "heart.circle")
Text("Manage Favorites")
}
}
// Settings
NavigationView {
VStack {
SettingsScreen(viewModel: viewModel)
}
.navigationTitle("Settings")
}
.tabItem {
VStack {
Image(systemName: "gearshape")
Text("Settings")
}
}
}
}
}
struct MainScreen_Previews: PreviewProvider {
static var previews: some View {
Group {
MainScreen(viewModel: ViewModel())
Landscape {
MainScreen(viewModel: ViewModel())
}
}
}
}
view rawMainScreen.swift hosted with ❤ by GitHub

다양한보기에 대한 자세한 내용은 아래에 표시되는 코드 목록에서 제공됩니다.

SearchBar.swift SwiftUI 코드 목록 :

//
// SearchBar.swift
// MediumArticleFlickrApp
//
// Created by Jody Abney on 12/29/20.
//
import SwiftUI
struct SearchBar: View {
@ObservedObject var viewModel: ViewModel
@State private var isEditing = false
var body: some View {
HStack {
TextField("Search ...", text: $viewModel.searchText)
.padding(7)
.padding(.horizontal, 25)
.background(Color(.systemGray6))
.cornerRadius(8)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 8)
if isEditing {
Button(action: {
viewModel.searchText = ""
}) {
Image(systemName: "multiply.circle.fill")
.foregroundColor(.gray)
.padding(.trailing, 8)
}
}
}
)
.padding(.horizontal, 10)
.onTapGesture {
self.isEditing = true
}
if isEditing {
Button(action: {
self.isEditing = false
viewModel.searchText = ""
}) {
Text("Cancel")
}
.padding(.trailing, 10)
.transition(.move(edge: .trailing))
.animation(.default)
}
}
}
}
struct SearchBar_Previews: PreviewProvider {
static var previews: some View {
SearchBar(viewModel: ViewModel())
.previewLayout(.sizeThatFits)
}
}
view rawSearchBar.swift hosted with ❤ by GitHub

의 SwiftUI 코드는 PhotoCategoryPicker.swiftPicker를 활용하여 세그먼트 컨트롤러를 구성하여 사용자가 Flickr 사진 스트림 (관심있는 사진, 최근 사진 또는 주변 사진)을 선택할 수 있도록합니다.

//
// PhotoCategoryPicker.swift
// MediumArticleFlickrApp
//
// Created by Jody Abney on 12/19/20.
//
import SwiftUI
struct PhotoCategoryPicker: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
Picker(selection: $viewModel.selectedCategory,
label: Text("Photo Category Picker")) {
Text("Interesting").tag(PhotoCategory.interestingness)
// only show Recent photos option if enabled
// via Settings screen
if viewModel.includeRecentPhotos {
Text("Recent").tag(PhotoCategory.recent)
}
Text("Near By").tag(PhotoCategory.nearBy)
}
.pickerStyle(SegmentedPickerStyle())
}
}
struct PhotoCategoryPicker_Previews: PreviewProvider {
static var previews: some View {
Group {
PhotoCategoryPicker(viewModel: ViewModel())
.previewLayout(.sizeThatFits)
PhotoCategoryPicker(viewModel: PreviewViewModel())
.previewLayout(.sizeThatFits)
}
}
}

다음 은 선택한 포토 스트림의 이미지를 표시 하기 위해 에서 PhotoGrid.swiftViewModel의 [Photo]배열을 사용하는 방법을 설명 하는 코드 목록입니다 LazyVGrid.

//
// PhotoGrid.swift
// MediumArticleFlickrApp
//
// Created by Jody Abney on 12/6/20.
//
import SwiftUI
struct PhotoGrid: View {
@ObservedObject var viewModel: ViewModel
var columnsAdaptive = [GridItem(.adaptive(minimum: 150, maximum: 300))]
var body: some View {
ScrollView {
LazyVGrid(columns: columnsAdaptive, content: {
ForEach(viewModel.photos) {
photo in
PhotoGridCell(viewModel: viewModel, photo: photo)
}
})
}
.padding()
}
}
struct PhotoGrid_Previews: PreviewProvider {
static var previews: some View {
PhotoGrid(viewModel: ViewModel())
}
}
view rawPhotoGrid.swift hosted with ❤ by GitHub

PhotoGridCell.swift코드 목록은 개별 사진 축소판이 사진 격자 내에 표시되고 다음을위한 탐색 방법으로 활성화되는 방법에 대한 세부 정보를 제공합니다 PhotoScreen.swift.

//
// PhotoGridCell.swift
// MediumArticleFlickrApp
//
// Created by Jody Abney on 12/6/20.
//
import KingfisherSwiftUI
import SwiftUI
struct PhotoGridCell: View {
@ObservedObject var viewModel: ViewModel
let photo: Photo
@State var isActive = false
var body: some View {
ZStack {
// Show a progress view until the photo overlays it
ProgressView()
// Display the photo
CellPhotoView(photo: photo)
// set up for tap navigation
.onTapGesture {
self.isActive.toggle()
}
.background(NavigationLink(
destination: PhotoScreen(viewModel: viewModel,
isFavorite: viewModel.isFavorite(photo: photo),
photo: photo),
isActive: $isActive) { EmptyView() }
)
}
.padding()
}
}
struct PhotoGridCell_Previews: PreviewProvider {
static var previews: some View {
PhotoGridCell(viewModel: ViewModel(), photo: Photo.default)
}
}
view rawPhotoGridCell.swift hosted with ❤ by GitHub

CellPhotoView.swift코드 목록은 KingfisherSwiftUIURL에서 사진 데이터를 디코딩하는 수단으로 Swift Package 를 활용하는 동시에 불필요한 데이터 전송을 방지하기 위해 이미지 캐싱을 활성화합니다.

//
// CellPhotoView.swift
// MediumArticleFlickrApp
//
// Created by Jody Abney on 12/21/20.
//
import KingfisherSwiftUI
import SwiftUI
struct CellPhotoView: View {
let photo: Photo
var body: some View {
// Display the photo
KFImage(photo.remoteURL)
// set photo display characteristics
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(10.0)
}
}
struct CellPhotoView_Previews: PreviewProvider {
static var previews: some View {
CellPhotoView(photo: Photo.default)
.previewLayout(.sizeThatFits)
}
}
view rawCellPhotoView.swift hosted with ❤ by GitHub

PhotoScreen (사진 상세 화면)

다음은 사용자가 사진 격자에서 이미지를 탭하면 사진과 해당 세부 정보가 어떻게 표시되는지 보여주는 PhotoScreen의 스크린 샷입니다.

스크린 샷 : Compact Horizontal Class Size (Jody P. Abney) 기반 PhotoScreen

다음에 대한 SwiftUI 코드 목록은 다음과 같습니다 PhotoScreen.swift.

//
// PhotoScreen.swift
// MediumArticleFlickrApp
//
// Created by Jody Abney on 12/20/20.
//
import MapKit
import SwiftUI
struct PhotoScreen: View {
// Handle device sizes to allow the views to change based on horizontal class size
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
@ObservedObject var viewModel: ViewModel
@State var isFavorite: Bool
let licenses = Licenses()
var photo: Photo
// photo content mode
@State var contentMode = ContentMode.fit
// Computed property for offset in overlaps
var offsetValue: CGFloat {
horizontalSizeClass == UserInterfaceSizeClass.compact ? -130 : 0
}
var body: some View {
if horizontalSizeClass == UserInterfaceSizeClass.compact {
VStack {
PhotoScreenView(contentMode: $contentMode,
photo: photo,
offsetValue: offsetValue)
if contentMode == .fit {
// Set up photo details view
PhotoDetails(photo: photo)
Spacer()
// Set up photo license view
PhotoLicenseView(license: licenses.getPhotoLicense(id: photo.license))
}
}
.navigationBarTitle(Text(photo.title))
.navigationBarItems(trailing: viewModel.authenticated ?
Button(action: {
if !isFavorite {
viewModel.favPhotos.append(photo)
} else {
let _ = viewModel.favPhotos.removeAll { (vmPhoto) -> Bool in
vmPhoto.id == photo.id
}
}
isFavorite.toggle()
// TODO: Implement actual add/remove fav functionality with ViewModel
} ) {
Image(systemName: isFavorite ? "heart.fill" : "heart")
} : nil )
} else {
HStack {
PhotoScreenView(contentMode: $contentMode,
photo: photo,
offsetValue: offsetValue)
if contentMode == .fit {
VStack {
// Set up photo details view
PhotoDetails(photo: photo)
Spacer()
// Set up photo license view
PhotoLicenseView(license: licenses.getPhotoLicense(id: photo.license))
}
.padding()
}
}
.navigationBarTitle(Text(photo.title))
.navigationBarItems(trailing: viewModel.authenticated ?
Button(action: {
if !isFavorite {
viewModel.favPhotos.append(photo)
} else {
let _ = viewModel.favPhotos.drop { (vmPhoto) -> Bool in
vmPhoto.id == photo.id
}
}
isFavorite.toggle()
// TODO: Implement actual add/remove fav functionality with ViewModel
} ) {
Image(systemName: isFavorite ? "heart.fill" : "heart")
} : nil )
}
}
}
struct PhotoScreen_Previews: PreviewProvider {
static var previews: some View {
Group {
PhotoScreen(viewModel: ViewModel(), isFavorite: false, photo: Photo.default)
.environment(\.horizontalSizeClass, UserInterfaceSizeClass.compact)
PhotoScreen(viewModel: ViewModel(), isFavorite: true, photo: Photo.default, contentMode: .fill)
.environment(\.horizontalSizeClass, UserInterfaceSizeClass.compact)
Landscape {
PhotoScreen(viewModel: ViewModel(), isFavorite: false, photo: Photo.default, contentMode: .fit)
.previewDevice("iPhone 12 Pro Max")
.environment(\.horizontalSizeClass, UserInterfaceSizeClass.regular)
}
Landscape {
PhotoScreen(viewModel: ViewModel(), isFavorite: true, photo: Photo.default, contentMode: .fill)
.previewDevice("iPhone 12 Pro Max")
.environment(\.horizontalSizeClass, UserInterfaceSizeClass.regular)
}
}
}
}
view rawPhotoScreen.swift hosted with ❤ by GitHub

사진 화면 레이아웃은 장치의 수평 클래스 크기에 따라 조정됩니다. 크기가 .compact이면 세로보기가 표시됩니다. 기기의 수평 클래스 크기가 .regular이면 사용 가능한 화면 공간을 더 잘 활용하기 위해 수평 레이아웃이 사용됩니다.

스크린 샷 : 정규 수평 클래스 크기를 기반으로 한 PhotoScreen (Jody P. Abney)

PhotoScreen.swift파일 내의 각 뷰 구성 요소에 대한 추가 개별 코드 목록을 게시하는 대신 이 기사 끝에서 사용할 수있는 전체 GitHub 저장소를 독자에게 참조 할 것입니다.

즐겨 찾기 화면 관리

스크린 샷 : 즐겨 찾기 관리 화면 (Jody P. Abney)

설정 화면

스크린 샷 : 설정 화면 (Jody P. Abney)

마무리

이 기사가 SwiftUI에 대한 귀하의 욕구를 불러 일으키고 Apple의 선언적 인터페이스 언어를 사용하여 앱을 개발해 보도록 영감을주기를 바랍니다. 내 SwiftUI 코드에 대한 더 자세한 정보를 얻으려면 내 GitHub.com 저장소를 탐색하는 것이 좋습니다.

무엇 향후 계획?

다음 기사에서는 JSON 데이터 모델링 및 네트워킹에 접근하는 방법을 설명합니다.

다음 시간까지 안전을 유지하고 계속 학습하세요!

전체 저장소는 아래 링크에서 사용할 수 있으며 독자는 앱에 포함 된 각 화면 및보기에 대한 자세한 내용을 탐색 할 수 있습니다 .

댓글 없음:

댓글 쓰기