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

SwiftUI - tutorial (Building Lists and Navigation and Generating Previews Dynamically)

by JeongUPark 2020. 7. 19.
반응형

오늘은 SwiftUI tutorial 그 두번째  Building List와 Navigation에 대해 작성해보겠습니다.

 

우선 지난 번에 작성한 project에 추가로 작성하도록 하겠습니다.

List

우선 먼저 List에 들어갈 데이터를 만들어 보도록 하겠습니다.

 

다음과 같이 PetInfo라는 swift 파일을 만듭니다.

import SwiftUI
import CoreLocation

struct PetInfo:  Hashable, Codable  {
    var id : Int
    var name : String
    fileprivate var imageName: String
    fileprivate var coordinates: Coordinates
    var category : Category
    var nickName : String
    
    enum Category : String, CaseIterable, Codable, Hashable {
        case cat  = "CAT"
        case dog = "DOG"
    }
    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
}

id/name/imageName/coordinates(위치)/category/nickName 이렇게 팻의 정보를 파씽 할 수 있도록 합니다.(사실 coordinates는 SwiftUI tutorial에서 공원으로 하고 있어서 그 위치를 위한 데이터지만, mapview를 위해 그대로 사용 하였습니다.)

 

위 코드를 보면 Hashable과 Codable 그리고 fileprivate라는 것이 있습니다. 

 

Hashable이라는 프로토콜을 통해 커스텀 구조 및 고유 값을 만들 수 있고 Codable은 Decodable과 Encodable프로토콜을 준수하는 타입(프로토콜)으로 json을 파싱 할 수 있게 해줍니다. (json으로 Pet정보를 만들꺼기 때문에 사용했습니다. 그리고 Hshable 없어도 코드는 잘 동작합니다.)

마지막으로 fileprivate 접근 제한자로 소스 파일 내에서만 접근이 가능하다.

 

그런데 말입니다. 여기까지 따라하다보면 먼가 이상하다는 것을 알 수 있습니다. ImageStore에서 error가 발생합니다. 왜 그럴까 고민을 해보다 답이 없어서 예제 파일을 봤더니!! Data.swift라는 파일에 따로 정의가 되어 있었습니다. tutorial에는 이 같은 부분이 설명이 없더군요.... (약 30분정도 헤멘것 같습니다.)

그 Data.swift를 보면

//
//  Data.swift
//  studySwiftUI
//
//  Created by JeongU Park on 2020/07/17.
//  Copyright © 2020 JeongU Park. All rights reserved.
//

import UIKit
import SwiftUI
import CoreLocation

let petData : [PetInfo] = load("petdata")

func load<T: Decodable>(_ filename: String) -> T {
    let data: Data
    
    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
        else {
            fatalError("Couldn't find \(filename) in main bundle.")
    }
    
    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }
    
    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}

final class ImageStore {
    typealias _ImageDictionary = [String: CGImage]
    fileprivate var images: _ImageDictionary = [:]

    fileprivate static var scale = 2
    
    static var shared = ImageStore()
    
    func image(name: String) -> Image {
        let index = _guaranteeImage(name: name)
        
        return Image(images.values[index], scale: CGFloat(ImageStore.scale), label: Text(name))
    }

    static func loadImage(name: String) -> CGImage {
        guard
            let url = Bundle.main.url(forResource: name, withExtension: "jpg"),
            let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
            let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
        else {
            fatalError("Couldn't load image \(name).jpg from main bundle.")
        }
        return image
    }
    
    fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index {
        if let index = images.index(forKey: name) { return index }
        
        images[name] = ImageStore.loadImage(name: name)
        return images.index(forKey: name)!
    }
}

이렇게 이미지를 불러오고(ImageStore) json 데이터를 파싱하는 부분(load)가 있습니다. 그리고 맨위에는 json 데이터를 불러와 파싱하고 저장하는 부분이 있습니다.

 

자 그럼 위의 Pet들에 대한 정보를 json으로 만들어보겠습니다. 우선 newFile을 선택하시고,  Emtpy를 선택하고 next를 누른 다음 petdata로 이름을 정합니다.

그다음 아래 데이터를 만들 petdata에 너어줍니다.

[
    {
        "name": "kiti",
        "category": "CAT",
        "nickName": "samll bady",
        "id": 1001,
        "coordinates": {
            "longitude": -116.166868,
            "latitude": 34.011286
        },
        "imageName": "cat"
    },
    {
        "name": "Bow",
        "category": "DOG",
        "nickName": "bestFriend",
        "id": 1002,
        "coordinates": {
            "longitude": -116.166868,
            "latitude": 34.011286
        },
        "imageName": "dog"
    },
]

(형식이 json이기 때문에 파일 형식이 .json일 필요가 없지만 신경 쓰이신다면 이름을 petdata.json으로 변경하시면 됩니다.)

 

자 그러고 위의 데이터를 통하여 List에 들어가 row를 만들어 보겠습니다.

 

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()
        }
    }
}


struct PetRow_Previews: PreviewProvider {
    static var previews: some View {
        PetRow(petInfo: petData[0])
    }
}

위와 같이 설정해 줍니다. 

petInfo를 받아올 객체를 만들고, HStack에서 petInfo의 데이터로 Row를 만들어 줍니다. 위에서 image의 정의를 보면 PetInfo.swift에 있는

    var image :Image{
        ImageStore.shared.image(name: imageName)
    }

이 부분인 것을 알 수 있습니다.

그리고 PreView에 아까 Data.swfit에서 만들 petData의 0번째 데이터를 불러와 확인해보면

이렇게 나타나는 것을 알 수 있습니다.

 

그런데 이 View는 리스트에 item이 될 부분인데 너무 큰것 같습니다. 그래서  아까 PreView부분을 다음과 같이 고치면

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

아래처럼 줄어들고

그리고 다음과 같이 추가하면

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

다음과 같이

고양이와 개가 나온느 것을 확인 할 수 있습니다.

그리고 위와 같이 각각의 Row의 사이즈를 정할 수 도 있고 group에 옵션을 줘서 동일한 조건을 줄 수 도 있습니다.

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

자 그럼 List에 Pet정보를 노출해 보도록 하겠습니다.

 

import SwiftUI

struct PetList: View {
    var body: some View {
        List{
            PetRow(petInfo: petData[0])
            PetRow(petInfo: petData[1])
        }
    }
}

struct PetList_Previews: PreviewProvider {
    static var previews: some View {
        PetList()
    }
}

이렇게 작성을 하면

고양이와 개의 정보가 List에서 나타나는 것을 확인 할 수 있습니다. 하지만 일일이 petData에 index를 주면서 List를 만드는건 너무 비효율 적이입니다. 그래서 다음과 같이 처리할 수 있습니다.

import SwiftUI

struct PetList: View {
    var body: some View {
        List(petData, id: \.id){ petInfo in
            PetRow(petInfo: petInfo)
        }
    }
}

struct PetList_Previews: PreviewProvider {
    static var previews: some View {
        PetList()
    }
}

petInfo에서 설정한 id를 통하여 List를 뿌려주는 것입니다. 특정할 수 있는 id가 있으면 그 id로 List에 데이터를 넣을 수 있습니다. 그래서 json과 petInfo를 쪼금 고쳐서 확인을 하면

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

4개의 항목이 보이는 것을 확인 할 수 있습니다. 그리고 만약에 특정할 id가 없다면 확장자에 Identifiable을 추가 해주면

struct PetInfo: Hashable, Codable ,Identifiable {

 List code가 다음과 같이 간단하게 변하고 위와 동일한 결과를 확인 할 수 있습니다,

struct PetList: View {
    var body: some View {
        List(petData){ petInfo in
            PetRow(petInfo: petInfo)
        }
    }
}

Navigation

이번에는 리스트에서 항목을 선택시 상세 화면으로 가도록 해보겠습니다.

 

우선 navigationView를 추가합니다. (그럼 preview 위에 공간이 생깁니다.)

import SwiftUI

struct PetList: View {
    var body: some View {
        NavigationView {
            List(petData){ petInfo in
                PetRow(petInfo: petInfo)
            }
        }
    }
}

struct PetList_Previews: PreviewProvider {
    static var previews: some View {
        PetList()
    }
}

그리고 Navigation에 title을 추가해주는데, 이게 또 tutorial은 NavigationView 끝나고 그 끝에 .navigationBarTitle( 하라고 되어있는데 그렇게 하면 PreView에서 아무것도 변하지 않습니다.  NavigationView안에서 .navigationBarTitle를 해주어야 제대로 동작합니다

이렇게 수정하면

struct PetList: View {
    var body: some View {
        NavigationView {
            List(petData){ petInfo in
                 PetRow(petInfo: petInfo)
                .navigationBarTitle(Text("Pets"))
            }
        }
    }
}

결과를 확인 할 수 있습니다.

그리고 itme을 클릭하고 다른 화면으로 넘어가기 위해

struct PetList: View {
    var body: some View {
        NavigationView {
            List(petData){ petInfo in
                NavigationLink(destination: ContentView()) {
                    PetRow(petInfo: petInfo)
                }
                .navigationBarTitle(Text("Pets"))
            }
        }
    }
}

위와 같이 NavigationLink를 추가해주면

각 항목들 옆에 > 표가 생긴것을 확인 할 수 있습니다.

PreView를 활성화 시키고 항목을 눌러보면 ContentView가 나타나는 것을 확인 할 수 있습니다.

그런데 List의 무슨 항목을 선택하던 위의 화면만 나타납니다. 그래서 항목에 맞도록 화면에 나타나기 위해 수정을 해보겠습니다.

지난번에 만들었던  CircleImageView를 Image를 받을 수 있도록 수정합니다.

import SwiftUI

struct CircleImageView: View {
    var image :Image
    var body: some View {
        image
            .clipShape(Circle())
            .overlay(
                Circle().stroke(Color.red, lineWidth: 10))
            .shadow(radius: 5)
        
    }
}

struct CircleImageView_Previews: PreviewProvider {
    static var previews: some View {
        CircleImageView(image: petData[1].image)
    }
}

그리고 위치 정보를 보여주는 View인  MapView에 CLLocationCoordinate2D 정보를 받을 수 있도록 수정합니다

import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
    var coordinate : CLLocationCoordinate2D
    func makeUIView(context: Context) -> MKMapView {
           MKMapView(frame: .zero)
       }

       func updateUIView(_ view: MKMapView, context: Context) {
           let coordinate = CLLocationCoordinate2D(
               latitude: 34.011286, longitude: -116.166868)
           let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
           let region = MKCoordinateRegion(center: coordinate, span: span)
           view.setRegion(region, animated: true)
       }
}

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView(coordinate: petData[1].locationCoordinate)
    }
}

마지막으로 상세정보를 보여주던 ContentView를 각 필요한 데이터를 받을 수 있도록 수정합니다. (PetInfo를 통해서 말이죠)

 

import SwiftUI

struct ContentView: View {
    var petInfo : PetInfo
    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){
                Text(petInfo.name)
                    .font(.title)
                    .foregroundColor(.green)
                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])
    }
}

이렇게 하고 PetList의 Preview에서 새를 클릭하면 새가 잘 나오는 것을 확인 할 수 있습니다.

 

마지막으로 Preview 컨트롤을 해보겠습니다.

 

Generating Previews Dynamically

위에 작성된 PetList의 PreView에 다음과 같이 추가를 하면 Preview의 device를 조절 할 수 있습니다.

struct PetList_Previews: PreviewProvider {
    static var previews: some View {
        PetList()    .previewDevice(PreviewDevice(rawValue: "iPhone XS"))
    }
}

이렇게 변경하면 다음과 같이 Preview에 iPhone XS가 나오는 것을 볼 수 있습니다.

그리고 여러개의 Preview를 보길 원한다면

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

이렇게 작성을 하면 다음과 같이 2개의 기종에 대한 PreView를 볼 수 있습니다.

 

 

반응형