본문 바로가기

카테고리 없음

SwiftUI Tutorials - 사용자 입력 다루기

SwiftUI Tutorials

사용자 입력 다루기

Landmark앱에서 사용자는 본인이 원하는 장소를 선호하는 장소로 표시할 수 있고 해당 장소들로만 리스트를 확인할 수 있습니다. 이러한 기능을 만들려면, 리스트를 전환하는 기능을 추가해야 합니다. 또한, 사용자가 선호하는 장소 랜드마크를 표시할 때 탭할 수 있는 별 모양 버튼을 추가할 것입니다.

Section 1

사용자의 선호 랜드마크 표시하기

사용자의 선호 장소를 한눈에 보여줄 수 있도록 목록을 개선해봅시다. LandmarkRow 뷰에 별을 추가하여 해당 랜드마크가 선호하는 곳임을 보여줄 것입니다.

A screenshot of a list of four landmarks. The top landmark row is highlighted and has a gold star to indicate that it is one of the user’s favorites.

Step 1

제공한 Xcode 프로젝트 중 starting point 프로젝트를 엽니다. 그리고 LandmarkRow.swift 파일을 프로젝트 네비게이터에서 선택합니다.

A screenshot of Xcode's Project navigator, with the LandmarkRow.swift file higlighted.

Step2

Spacer() 아래에 선호 장소인지 확인하는 if 문을 작성하고 별 이미지를 추가합니다. SwiftUI의 블록에서 if문은 조건에 따라 뷰를 포함할 것인지를 결정할 수 있습니다.

import SwiftUI

struct LandmarkRow: View {
  var landmark: Landmark

  var body: some View {
    HStack {
      landmark.image
          .resizable()
          .frame(width: 50, height: 50)
      Text(landmark.name)
      Spacer()
      // 선호 장소임을 표시하는 if문추가
      if landmark.isFavorite {
        Image(systemName: "star.fill")
            .imageScale(.medium)
      }
      //--
    }
  }
}
...

An image of two views in the preview in Xcode. The top view has an empty star at its right edge.

Step 3

시스템 이미지(system image)는 벡터를 기반으로 하였기 때문에, 여러분은 foregroundColor(_:) 수정자 메서드를 이용하여 벡터 이미지의 컬러를 바꿀 수 있습니다.

별은 랜드마크의 Favorite 속성이 true인지를 표현합니다. 이후의 튜토리얼에서 그 속성을 어떻게 수정하는지를 알아볼 것입니다.

import SwiftUI

struct LandmarkRow: View {
  var landmark: Landmark

  var body: some View {
    HStack {
      landmark.image
          .resizable()
          .frame(width:50, height: 50)
      Text(landmark.name)
      Spacer()

      if landmark.isFavorite {
        Image(systemName: "star.fill")
            .imageScale(.medium)
            // 배경색 변경 수정자 추가
            .forgroundColor(.yellow)
          //--
      }
    }
  }
}
...

An image of two views in the preview in Xcode. The top view has a gold star at its right edge.

Section 2

리스트 뷰 필터하기

여러분은 리스트 뷰를 커스터마이징하여 모든 랜드마크를 보여주거나 사용자의 선호장소를 표시할 수 있습니다. 이 기능을 구현하려면 LandmarkList 뷰에 @State 어노테이션이 필요합니다.

@State가 붙은 상태는 값이거나 값의 집합입니다. 이것은 변할 수 있으며 다른 뷰의 동작이나 내용 혹은 레이아웃에 영향을 미칩니다. 여러분은 @State 속성을 이용하여 뷰에 상태를 추가할 수 있습니다.

A flow diagram that shows the Show Favorites Only item flowing through a filter and into the landmark list view.

Step 1

프로젝트 네비게이터에서 LandmarkList.swift을 선택합니다. @State 속성을 FavoritesOnly의 이름으로 LandmarkList뷰에 추가합니다.

import SwiftUI

struct LandmarkList: View {
  // @State 속성 추가
  @State var showFavortiesOnly = false
  // --
  var body: some View {
    NavigationView {
      List(landmarkData) {     landmark in
            NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
            LandmarkRow(landmark: landmark)
          }
      }
      .navigationBarTitle(Text("Landmarks"))
    }
  }
}

A screenshot of the preview in Xcode, showing a list of landmarks with the first, third, and fourth landmarks being marked as favorites with a gold star at the right edge of the row.

Step 2

Resume버튼을 눌러 캔버스를 새로고침합니다.

속성을 추가하거나 수정하는 등, 뷰의 구조를 변경했다면 캔버스를 직접 새로고침해야할 수도 있습니다.

A screenshot of the preview in Xcode, with the Resume button showing.

Step 3

showFavoritesOnly 속성과 landmark.isFavorite 값을 체크하여 랜드마크의 리스트를 필터링하세요.

import SwiftUI

struct LandmarkList: View {
  @State var showFavoritesOnly = false

  var body: some View {
    NavigationView {
      List(landmarkData) { landmark in
                // 선호장소만 표시되는 기능을 끔, 또는 켰을 경우 해당 장소가 선호 장소임
            if !self.showFavoritesOnly || landmark.isFavorite {
            NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
              LandmarkRow(landmark: landmark)
            }
          }    
                //--
      }
    }
    .navigationBarTitle(Text("Landmarks"))
  }
}

Section 3

상태를 토글하기 위한 컨트롤 추가

사용자에게 리스트를 선호 장소만 필터링할 수 있도록 하려면, showFavorties의 값을 변경할 수 있는 컨트롤을 추가해야 합니다. 이 값을 변경하는 것은 토글 컨트롤에 _바인딩_을 전달하는 것입니다.

_바인딩(binding)_은 변경될 수 있는 상태(mutable state)에 대한 참조입니다.사용자가 토글을 탭하여 꺼짐에서 켬으로 바거나 끄게될 때, 컨트롤은 바인딩을 이용하여 그 뷰에 대한 상태를 똑같이 수정해줘야 합니다.

A flow diagram that shows the Show Favorites Only item being passed through a filter to the landmarks view. At the top of the view, the row labeled Favorites Only is highlighted, with the switch toggled On.

Step 1

중첩된 ForEach그룹을 생성하여 landmarks를 행으로 변형합니다.

리스트안의 정적 뷰를 동적 뷰와 합치려고 하거나, 동적 뷰의 서로 다른 그룹들을 합치고 싶다면 List에 컬렉션을 넘기는 대신, ForEach를 사용하세요.

import SwiftUI

struct LandmarkList: View {
  @State var showFavoritesOnly = true

  var body: some View {
    NavigationView {
      // List대신 Foreach가 배열을 순회하도록 함
      List {
        ForEach(landmarkData) { landmark in
              if !self.showFavoritesOnly || landmark.isFavorite {
              NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                LandmarkRow(landmark: landmark)
              }
            }
        }
      }
      // --
      .navigationBarTitle(Text("Landmark"))
    }
  }
}

Step 2

Toggle 뷰를 List 뷰의 첫 자식 뷰로 추가합니다. 그리고 FavortiesOnly를 보여주기 위해, 바인딩을 넘겨줍니다.

$을 상태 변수 맨 앞에 붙여서 바인딩에 접근할 수 있도록 해줍니다.

import SwiftUI

struct LandmarkList: View {
  @State var showFavortiesOnly = true

  var body: some View {
    NavigationView {
      List {
        // 토글 뷰를 추가
        Toggle(isOn: $showFavortiesOnly) {
          Text("Favorites Only")
        }
        // --
        ForEach(landmarkData) { landmark in
              if !self.showFavortiesOnly || landmark.isFavorite {
              NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                LandmarkRow(landmark: landmark)
              }
            }
        }
      }
      .navigationBarTitle(Text("Landmarks"))

    }
  }
}
...

Step 3

라이브 프리뷰를 사용하여 토글을 탭할 때 새로운 기능이 동작하는지 확인하세요.

Section 4

저장을 위한 옵저버블 객체사용하기

사용자가 특정한 랜드마크를 선호지역으로 설정하도록 하기 위해, 여러분은 먼저, _옵저버블 객체(observable object)_에 랜드마크 데이터를 저장해야 합니다.

옵저버블 객체는 커스텀 오브젝트로서, 뷰와 데이터를 연결할 수 있습니다. SwiftUI는 뷰에 영향을 줄 수 있는 옵저버블 객체의 변경을 감지하여 뷰에 정확한 상태를 출력해줍니다.

A diagram that shows the binding options for User Data.

Step 1

UserData.swift라는 새로운 스위프트 파일을 생성합니다.

import SwiftUI    

Step2

UserData 클래스가 ObservableObject 프로토콜을 따르도록 새로운 모델 타입을 선언합니다. 이 프로토콜은 Combine 프레임워크에 있습니다.

SwiftUI는 여러분의 옵저버블 객체를 감시하고 그 데이터가 변경되면 뷰를 수정합니다.

import SwiftUI
import Combine

final class UserData: ObservableObject {

}

Step 3

landmarksshowFavortiesOnly 을 저장 용 속성으로 추가합니다. 또한 초기값을 동시에 부여하도록 합니다.

import SwiftUI
import Combine

final class UserData: ObservableObject {
  var showFavortiesOnly = false
  var landmarks = landmarkData
}

옵저버블 객체는 그것의 데이터가 변경되면 다시 갱신(publish)되어야 합니다. 따라서 그것을 사용하는 객체가 그 변화를 알아야 합니다.

Step 4

@Published 특성을 각 속성에 추가합니다.

// UserData.swift
import SwiftUI
import Combine

final class UserData: ObservableObject {
  @Published var showFavortiesOnly = false
  @Published var landmarks = landmarkData
}

Section 5

뷰에 모델 오브젝트 적용하기

이제 UserData객체를 생성하였으므로 뷰에 이것을 적용하여 앱에 데이터를 저장할 수 있습니다.

A flow diagram that shows an Environment block flowing into the landmarks list, which then flows into the Landmark ID view that shows a landmark's details and map view.

Step 1

LandmarkList.swiftshow FavoritesOnly로 선언된 부분을 @EnvironmentObject 속성으로 바꾸어 줍니다. 그리고 environmentObject(_:) 수정자를 프리뷰에 추가합니다.

userData 속성은 environmentObject(_:) 수정자가 부모에 적용된다면 자동으로 값을 얻어옵니다.

import SwiftUI

struct LandmarkList: View {

  @EnvironmentObject var userData: UserData

  var body: some View {
    NavigationView {
      List {
        Toggle(isOn: $showFavoritesOnly) {
          Text("Favorites only")
        }

        ForEach(landmarkData) { landmark in
          if !self.showFavoritesOnly || landmark.isFavorite {
            NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
              LandmarkRow(landmark: landmark)
            }
          }
        }
      }
      .navigationBarTitle(Text("Landmarks"))
    }
  }
}

struct LandmarkList_Previews: PreviewProvider {
  static var previews: some View {
    LandmarkList()
        .environmentObject(UserData())
  }
}

Step 2

userData의 속성에 접근하도록 showFavoritesOnly을 바꾸어줍니다.

@State 속성과 같이, $을 앞에 추가하여 userData객체의 멤버의 바인딩에 접근할 수 있습니다.

import SwiftUI

struct LandmarkList: View {
  @EnvironmentObject var userData: UserData

  var body: some View {
    NavigationView {
      List {
        Toggle(isOn: $userData.showFavoritesOnly) { // 바인딩에 접근
          Text("Favorites only")
        }

          ForEach(userData.landmark) { landmark in 
          if !self.userData.showFavortiesOnly || landmark.isFavorites {
            NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
              LandmarkRow(landmark: landmark)
            }
          }
        }
      }
      .navigationBarTitle(Text("Landmarks"))
    }
  }
}

Step 3

ForEach 구문에서 userData.landmarks를 데이터로 사용하도록 수정하세요.

import SwiftUI

struct LandmarkList: View {
  @EnvironmentObject var userData: UserData

  var body: some View {
    NavigationView {
      List {
        Toggle(isOn: $userData.showFavoritesOnly) {
          Text("Favorites only")
        }
        // 옵저버블 객체인 userData로 접근하도록 수정
        ForEach(userData.landmarks) { landmark in
          if !.self.userData.showFavrotiesOnly || landmark.isFavorites {
            NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
              LandmarkRow(landmark: landmark)
            }
          }
        }
      }
      .navigationBarTitle(Text("Landmarks"))
    }
  }
}

Step 4

SceneDelegate.swift에서 environmentObject(_:) 수정자 메서드에 LandmarkList를 추가합니다.

만약 여러분이 프리뷰를 사용하지 않고, Landmark앱을 시뮬레이터나 기기에서 실행할 때, 이 코드가 LandmarkList로 하여금 UserData 객체를 해당 환경에서 가져갈 수 있도록 합니다.

import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Use a UIHostingController as window root view controller
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(
                rootView: LandmarkList()
                    .environmentObject(UserData())
            )
            self.window = window
            window.makeKeyAndVisible()
        }
    }

    // ...
}

Step 5

LandmarkDetail 뷰를 수정하여 UserData 객체를 가지고 동작하도록 합니다.

이후, 랜드마크의 선호 상태에 접근하고 그것을 갱신할 때, landmarkIndex를 사용할 것입니다. 이것을 사용하여 데이터가 정확한 값에 접근할 수 있습니다.

import SwiftUI

struct LandmarkDetail: View {
  @EnvironmentObject var userData: UserData // 옵저버블 객체로 변경

  var landmark: Landmark

  // 랜드마크 favorite 속성에 접근
  var landmarkIndex: Int {
    userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
  }
  // --
  var body: some View {
    VStack {
      MapView(coordinate: landmark.locationCoordinate)
          .edgesIgnoringSafeArea(.top)
          .frame(height: 300)
      CircleImage(image: landmark.image)
          .offset(y: -130)
          .padding(.bottom, -130)

      VStack(alignment: .leading) {
        Text(landmark.name)
            .font(.title)
        HStack(alignment: .top) {
          Text(landmark.park)
              .font(.subheadline)
          Spacer()
          Text(landmark.state)
              .font(.subheadline)
        }
      }
      .padding()

      Spcaer()
    }
    .navigationBarTitle(Text(landmark.name), displayMode: .inline)
  }
}

struct LandmarkDetail_Preview: PreviewProvider {
  static var previews: some View {
    LandmarkDetail(landmark: landmarkData[0])
        .environmentObject(UserData())
  }
}

Step 6

LandmarkList.swift 로 돌아와서 수정한 코드가 정상적으로 동작하는지 확인합니다.

A screenshot showing the preview in Xcode. The Live Preview button is highlighted, and the list of landmarks is filtered to show only the three favorites.

Section 6

랜드마크 선호 버튼 생성하기

랜드마크 앱은 이제 필터링되거나 필터링되지 않은 리스트를 보여줄 수 있습니다. 사용자가 선호지역을 추가하거나 삭제하도록 하려면, 여러분은 랜드마크 상세 뷰에서 선호 버튼을 추가하도록 해야 합니다.

A flow diagram that shows the bindings for the toggle states of a switch, with a False toggle for Add to Favorites, and a True toggle for Remove from Favorites.

Step 1

LandmarkDetail.swift을 열고, HStack안에 랜드마크의 이름을 추가하도록 합니다.

// LandmarkDetail.swift
import  SwiftUI

struct LandmarkDetail: View {
  @EnvironmentObject var userData: UserData
  var landmark: Landmark

  var landmarkIndex: Int {
    userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
  }

  var body: some View {
    VStack {
      MapView(coordinate: landmark.locationCoordinate)
          .edgesIgnoringSafeArea(.top)
          .frame(height: 300)

      CircleImage(image: landmark.iamge)
          .offset(y: -130)
          .padding(.bottom, -130)

      VStack(alignment: .leading) {
        // 랜드마크 제목과 별 표시를 담을 HStack 추가
        HStack {
          Text(landmark.name)
              .font(.title)
        }
        // --
        HStack(alignment: .top) {
          Text(landmark.park)
              .font(.subheadline)
          Spacer()
          Text(landmark.state)
              .font(.subheadline)
        }
      }
      .padding()

      Spacer()

    }
    .navigationBarTitle(Text(landmark.name), displayMode: .inline)
  }
}

struct LandmarkDetail_Preivew: PreviewProvider {
  static var previews: some View {
    LandmarkDetail(landmark: landmarkData[0])
        .environmentObject(UserData())
  }
}

Step 2

랜드마크의 이름 옆에 새로운 버튼을 생성합니다. if-else 조건 문을 사용하여 랜드마크가 선호 지역인지를 알려주는 이미지를 제공하도록 할 것입니다.

버튼의 action 절의 코드는 landmarkIndexuserData 객체를 사용하여 랜드마크를 업데이트합니다.

import SwiftUI

struct LandmarkDetail: View {
  @EnvironmentObject var userData: UserData
  var landmark: Landmark

  var landmarkIndex: Int {
    userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
  }

  var body: some View {
    VStack {
      MapView(coordinate: landmark.locationCoordinate)
          .edgesIgonoringSafeArea(.top)
          .frame(height: 300)

      CircleImage(image: landmark.image)
          .offset(y: -130)
          .padding(.bottom, -130)

      VStack(alignment: .leading) {
        HStack {
          Text(landmark.name)
              .font(.title)
          // 별모양 버튼 추가
          Button(action: {
            self.userData.landmarks[self.landmarkIndex].isFavorite.toggle()
          }) {
            if self.userData.landmarks[self.landmarkIndex].isFavorite {
              Image(systemName: "star.fill")
                  .forgroundColor(Color.yellow)
            } else {
              Image(systemName: "star")
                  .foregroundColor(Color.gray)
            }
          }
          // --

        }
        ...
      }
    }
  }
}

Step 3

LandmarkList.swift로 전환하여 라이브 프리뷰를 실행합니다.

리스트에서 상세 뷰까지를 검사하고 버튼을 탭해보세요. 이러한 변경 사항들이 리스트에 다시 돌아올 때에도 유지됩니다. 왜냐하면 두개의 뷰는 같은 모델 객체에 접근하므로 두 개의 뷰는 일관성을 유지할 수 있는 것 입니다.


Q1) 데이터를 뷰 계층까지 전달하도록 하는 것은?

A1) @EnvironmentObject(x) environmentObject(_:) 수정자(o)

Q2) 바인딩의 역할은?

A2) 여러 뷰들을 서로 연결하여 그 뷰들이 같은 데이터를 받을 수 있도록 함, (X)

  • 값 자체를 의미하고, 값을 변경하는 방법을 의미함
  • 바인딩은 값을 저장하는 것을 제어함 따라서 다른 뷰들이 그 값을 읽고 쓰기 위함

원문 : https://developer.apple.com/tutorials/swiftui/handling-user-input