본문 바로가기
2023년 이전/iOS

SwiftUI - tutorial - Handling User Input

by JeongUPark 2020. 7. 19.
반응형

Apple에서 제공하는 Swift UI를 공부하면서 정리한 내용 입니다.

 

이번에는 사용자 액션에 따른 동작을 만들어 보도록 하겠습니다. 만들 내용은 즐겨 찾기 추가와 , 즐겨찾기 항목만 보여주기 입니다.

 

Favorite 추가

우선 List에 있는 항목에 Favorite를 구분할 수 있는 별을 추가해보겠습니다.

자 우선 Favorite를 petData 파일의 json에 추가 합니다.

[
    {
        "name": "kiti",
        "category": "CAT",
        "nickName": "samll bady",
        "id": 1001,
        "isFavorite" : true,
        "coordinates": {
            "longitude": -116.166868,
            "latitude": 34.011286
        },
        "imageName": "cat"
    },
    {
        "name": "Bow",
        "category": "DOG",
        "nickName": "bestFriend",
        "id": 1002,
        "isFavorite" : true,
        "coordinates": {
            "longitude": -116.166868,
            "latitude": 34.011286
        },
        "imageName": "dog"
    },
    {
        "name": "bul",
        "category": "CAW",
        "nickName": "red bul",
        "id": 1003,
        "isFavorite" : true,
        "coordinates": {
            "longitude": -116.166868,
            "latitude": 34.011286
        },
        "imageName": "caw"
    },
    {
        "name": "pipi",
        "category": "BRID",
        "nickName": "phoenix",
        "id": 1004,
        "isFavorite" : true,
        "coordinates": {
            "longitude": -116.166868,
            "latitude": 34.011286
        },
        "imageName": "brid"
    }
]

true로 해준 이유는 isFavorite가 Ture일 때를 우선 만들기 위함입니다. 그럼 PetInfo.swift에 isFavorite를 추가하고 PetRow.swift를 다음과 같이 수정합니다.

import SwiftUI
import CoreLocation

struct PetInfo: Hashable, Codable ,Identifiable {
    var id : Int
    var name : String
    fileprivate var imageName: String
    fileprivate var coordinates: Coordinates
    var category : Category
    var nickName : String
    var isFavorite: Bool = false
    enum Category : String, CaseIterable, Codable, Hashable {
        case cat  = "CAT"
        case dog = "DOG"
        case brid = "BRID"
        case caw = "CAW"
    }
    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(
            latitude: coordinates.latitude,
            longitude: coordinates.longitude)
    }
}
extension PetInfo {
    var image :Image{
        ImageStore.shared.image(name: imageName)
    }
}
struct Coordinates:  Hashable,Codable{
    var latitude: Double
    var longitude: Double
}
import SwiftUI

struct PetRow: View {
    
    var petInfo : PetInfo
    
    var body: some View {
        HStack{
            petInfo.image
                .resizable()
                .frame(width: 50, height: 50)
            Text("Name : \(petInfo.name) and Nick Name: \(petInfo.nickName) ")
            Spacer()
            
            if petInfo.isFavorite {
                Image(systemName: "star.fill")
                    .imageScale(.medium)
                    .foregroundColor(.yellow)
            }
        }
    }
}


struct PetRow_Previews: PreviewProvider {
    static var previews: some View {
        Group{
            PetRow(petInfo: petData[0])
            PetRow(petInfo: petData[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}

그럼 다음과 같이 Row에서 왼쪽 끝에 노란 별을 볼수 있습니다.

그 다음 filter 기능을 추가 해보겠습니다. 이때 사용하는 것은 @State 입니다. @State는 시간이 지남에 따라 변경 될 수 있고 보기의 동작, 컨텐츠 또는 레이아웃에 영향을 미치는 값 또는 값 세트입니다. 그래서 이 @State를 PetList.swift에 추가 합니다. isFavorite가 true 일 떄 Row를 보여 줍니다. (근데 이떄 showFavoritesOnly 가 false이기 때문에 모든 항목이 보이게 됩니다)

import SwiftUI

struct PetList: View {
    @State var showFavoritesOnly = false
    var body: some View {
        NavigationView {
            List(petData){ petInfo in
                if !self.showFavoritesOnly || petInfo.isFavorite {
                    
                    NavigationLink(destination: ContentView(petInfo: petInfo)) {
                        PetRow(petInfo: petInfo)
                    }
                    .navigationBarTitle(Text("Pets"))
                }
            }
        }
    }
}

struct PetList_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(["iPhone SE", "iPhone XS Max"], id: \.self) { deviceName in
            PetList()
                .previewDevice(PreviewDevice(rawValue: deviceName))
        }
    }
}

별이 변경된 이유는 petdata의 json 파일에서 isFavorite를 조정 했기 떄문입니다.

만일 showFavoritesOnly 가 true면 다음과 같이 나타납니다.

위와 같이 공백인 부분이 나오면 이상하기 공백없이 isFavorite가 true 항목만 표기하기 위해서 PetList.swift를 다음과 같이 수정 하였습니다.

import SwiftUI

struct PetList: View {
    @State var showFavoritesOnly = true
    var body: some View {
        NavigationView {
            List{
                ForEach(petData){ petInfo in
                    if !self.showFavoritesOnly || petInfo.isFavorite {
                        
                        NavigationLink(destination: ContentView(petInfo: petInfo)) {
                            PetRow(petInfo: petInfo)
                        }
                        .navigationBarTitle(Text("Pets"))
                    }
                }
            }
        }
    }
}

struct PetList_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(["iPhone SE", "iPhone XS Max"], id: \.self) { deviceName in
            PetList()
                .previewDevice(PreviewDevice(rawValue: deviceName))
        }
    }
}

forEach를 사용하여 개별 동작을 시키면 다음과 같이 공백 없이 나타납니다.

그리고 토글을 추가하여 showFavoritesOnly의 값을 컨트롤 할 수 있는 Toggle을 추가합니다.

import SwiftUI

struct PetList: View {
    @State var showFavoritesOnly = true
    var body: some View {
        NavigationView {
            List{
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites only")
                }
                ForEach(petData){ petInfo in
                    if !self.showFavoritesOnly || petInfo.isFavorite {
                        
                        NavigationLink(destination: ContentView(petInfo: petInfo)) {
                            PetRow(petInfo: petInfo)
                        }
                        .navigationBarTitle(Text("Pets"))
                    }
                }
            }
        }
    }
}

Toggle에 $showFavoritesOnly를 썼기 때문에 따로 설정할 것 없이 Toggle을 움직일 때마다 값이 변경하게 됩니다. 그래서 동작시켜 보면

이렇게 변경되는 것을 확인 할 수 있습니다. 즉 필터 기능을 완성하였습니다.

 

다음으로는  특정 Pet들을 컨트롤 할 수 있도록 Pet데이터를 ObservableObject 객체에 저장합니다.

import SwiftUI
import Combine

final class UserData: ObservableObject  {
    @Published var showFavoritesOnly = false
    @Published var pet = petData
}

우선 ObservableObject을 extend함으로서 SwiftUI는 UserData를 구독하고 , 데이터 변화에 의해 새로고침이 필요할 경우 view를 업데이트 합니다. 그리고 @Published를 붙여 줌으로써 구독자들에게 데이터 변화를 알려주게 됩니다. (이 구독과 발행은 Rx프로그래밍 개념을 익혀보시면 더 쉽게 이해 할 수 있습니다.)

 

이렇게 만든 UserData를 PetList.swift에 적용 합니다.(showFavoritesOnly가 UserData에 있기 때문에 showFavoritesOnly대신 UserData를 사용합니다., 그리고 petData도 UserData의 pets로 대채 할 수 있습니다.)

import SwiftUI

struct PetList: View {
    @EnvironmentObject var userData: UserData
    var body: some View {
        NavigationView {
            List{
                Toggle(isOn: $userData.showFavoritesOnly) {
                    Text("Favorites only")
                }
                ForEach(userData.pets){ petInfo in
                    if !self.userData.showFavoritesOnly || petInfo.isFavorite {
                        
                        NavigationLink(destination: ContentView(petInfo: petInfo)) {
                            PetRow(petInfo: petInfo)
                        }
                        .navigationBarTitle(Text("Pets"))
                    }
                }
            }
        }
    }
}

struct PetList_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(["iPhone SE", "iPhone XS Max"], id: \.self) { deviceName in
            PetList()
            .environmentObject(UserData())
                .previewDevice(PreviewDevice(rawValue: deviceName))
        }
    }
}

 그런데 이렇게 수정 후 PreView를 보면 crash가 발생 합니다. 그 이유는 SceneDelegate에 window에 petList와 environmentObject를 설정해주지 않아서 그렇습니다. 그래서 SceneDelegate의 sceen을 다음과 같이 수정 합니다.

    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).

        // Create the SwiftUI view that provides the window contents.
        let contentView =  PetList()

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

그리고 프리뷰를 보면 정상적으로 동작하는 것을 볼 수 있습니다.

 

다음으로는 Favorite 버튼을 만들어 추가/취소 할 수 있도록 해보겠습니다.

import SwiftUI

struct ContentView: View {
    
    @EnvironmentObject var userData: UserData
    var petInfo : PetInfo
    var petIndex: Int {
         userData.pets.firstIndex(where: { $0.id == petInfo.id })!
     }
    var body: some View {
        VStack {
            MapView(coordinate: petInfo.locationCoordinate).frame(height: 300).edgesIgnoringSafeArea(.top)
            CircleImageView(image: petInfo.image)
                .offset(y:-100)
                .padding(.bottom, -130)
            

            VStack(alignment:.leading){
                HStack{
                Text(petInfo.name)
                    .font(.title)
                    .foregroundColor(.green)
                    
                    Button(action: {
                        self.userData.pets[self.petIndex].isFavorite.toggle()
                    }) {
                        if self.userData.pets[self.petIndex].isFavorite {
                            Image(systemName: "star.fill")
                                .foregroundColor(Color.yellow)
                        } else {
                            Image(systemName: "star")
                                .foregroundColor(Color.gray)
                        }
                    }
                }
                HStack {
                    Text(petInfo.nickName)
                        .font(.subheadline)
                    Spacer()
                    
                    Text(petInfo.category.rawValue)
                        .font(.subheadline)
                }
            }.padding()
            
            Spacer()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(petInfo: petData[0]).environmentObject(UserData())
    }
}

UserData를 받아와서 그 업데이트에 대한 데이터를 ContentView에서 노출하고, ContentView의 변경 사항을 PetList에 반영됩니다.

 

위의 Code에서 

    var petIndex: Int {
         userData.pets.firstIndex(where: { $0.id == petInfo.id })!
     }

이 부분은 pets에서 petInfo의 id가 동일한 첫번째 항목의 index를 반환합니다.

그리고 Pet이름 옆에 Favorite button을 추가하여 사용자 액션에 따른 동작을 하도록 합니다. 위의 Code를 적용하고 PetList에 가서 PreView를 활성화 하면 Favorite button이 동작하는 것을 확인 할 수 있습니다. 

 

공부를 하다 MVVM 또는 반응형 개념이 나와서 당황했지만 매우 훌륭한 기능인 것 같습니다.

반응형