SwiftUI Map App

Woozoo·2023년 1월 26일
0

[SwiftUI]

목록 보기
22/26

Project setup

프로젝트 새로 만들어주고 Assets 다운 받은 콘텐츠로 바꿔줌


location DataService라는 파일이 있는데 Location 모델이 어떤 모양일지 대충 적혀있음
참고해서 Location 모델을 만들어보자

import Foundation
import MapKit

struct Location {    
    let name: String
    let cityName: String
    let coordinates: CLLocationCoordinate2D
    let description: String
    let imageNames: [String]
    let link: String
}

DataServices라는 폴더를 만들고 LocationDataService를 넣어줌
보통은 백엔드에서 처리하는 데이터모델을 저런식으로 구성하는데
지금은 라이브 서비스 앱은 아니지만 임시로 비슷하게 만들어줌

Create ViewModel with @StateObject and @EnvironmentObject


뷰모델을 하나 만들어줌
이따가는 @EnvironmentObject로 바꿀 예정

class LocationViewModel: ObservableObject {
    
    @Published var locations: [Location]
    
    init() {
        self.locations = LocationsDataService.locations
    }
}

init 될 때 LocationDataService에서 전역으로 설정한 데이터를 가져옴
그리고 vm.locations를 기반으로 ForEach 뷰를 만들어주는데 그러려면 Location 모델이 Identifiable 해야한다

일반적으로 이렇게 id를 구성해줬지만 이번엔 다르게 해보자


name과 cityName의 조합으로 id를 구성해줌


그리고 들어온 location들의 이름을 Text뷰로 표현!

이전에 작성했던 뷰모델을 EnvironmentObject로 바꿔주자

SwiftUI Map from MapKit


기존에 있던 리스트뷰는 지우고 Map을 이제 넣어보자


mapRegion이라는 MKCoordinateRegion을 만들어줌
center값은 LocationsDataService에 있는 임의의 데이터 가져옴
span은 맵이 줌인 되어 있을지 줌아웃되어있을지 정해주는 거임


요호 맵이 나온다

근데 지금 문제가 하나 있음
이대로면 뷰가 로케이션을 들고 있게됨
뷰모델로 옮겨주면 좋을거 같은데

mapLocation이라는 Location을 하나 선언해줌
그리고 init에서 첫번째 엘리먼트를 넣어줬다

import Foundation
import MapKit
import SwiftUI

class LocationsViewModel: ObservableObject {
    // All loaded locations
    @Published var locations: [Location]
    
    // Current Location on map
    @Published var mapLocation: Location
    
    @Published var mapRegion: MKCoordinateRegion = MKCoordinateRegion()
    let mapSpan =  MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
    
    init() {
        let locations = LocationsDataService.locations
        self.locations = locations
        self.mapLocation = locations.first!
        self.updateMapRegion(location: locations.first!)
    }
    
    private func updateMapRegion(location: Location) {
        withAnimation(.easeInOut) {
            mapRegion = MKCoordinateRegion(center: location.coordinates,
                                           span: mapSpan)
        }
    }
}

Map에서 바인딩될 mapRegion도 만들고
init될 때 update되는 메소드도 만들어줌 (애니메이션과함께)


다시 뷰로와서 기존에 있던 mapRegion을 뷰모델에 있는걸로 바꿔주면됨

뷰모델로 돌아와서 Location모델이 업데이트 될 때 mapRegion도 자동으로 업뎃해주고 싶음

mapLocation에 프로퍼티옵저버를 이용해서 mapRegion도 업뎃되게 해주면 된다!!


body안에 Text를 만들어줌
근데 이러면 바디가 너무 읽기힘드니까 extension으로 따로 빼두자



그리고 이렇게 화살표 누르면 아래로 뷰가 뜨게 하고 싶음

struct LocationsListView: View {
    
    @EnvironmentObject private var vm: LocationsViewModel
    
    var body: some View {
        List {
            ForEach(vm.locations) { location in
                HStack {
                    if let imageName = location.imageNames.first {
                        Image(imageName)
                            .resizable()
                            .scaledToFill()
                            .frame(width: 45, height: 45)
                            .cornerRadius(10)
                    }
                    
                    VStack(alignment: .leading) {
                        Text(location.name)
                            .font(.headline)
                        Text(location.cityName)
                            .font(.subheadline)
                    }
                    .frame(maxWidth: .infinity, alignment: .leading)  
                }
            }
        }
    }
}

HStack 정리해주고 싶음

extension으로 빼야될 것 같은데 현재 HStack은 location이라는 값을 받아서 사용하고 있다

메소드로 location을 받아서 some View를 return 하게 해주면 됨

오케이! 그리고 호출해주면 됩니다


다시 로케이션 뷰로 돌아와서 방금 작성해준 리스트뷰를 불러오면 되는데 스타일을 바꾸고 싶다!
핀셋모양 클릭하면 프리뷰를 고정해서 다른 코드파일을 열어도 프리뷰 고정이 가능함


요렇게

이제 뷰가 늘어나는 것만 구현해주면 될 것 같다
뷰모델로 돌아와서 Bool값을 하나 만들고 animation 메소드도 작성해줌
참 private은 빼줘야함!!

다시 LocationView로 돌아와서

기존에 있던 텍스트뷰 버튼으로 만들어줌

화살표 모양을 직접바꿔줄거라고 생각했는데 Image에 rotationEffect를 넣어서 애니메이션을 만들어줌

남은건 ListView에서 아이템이 클릭되었을 때 맵에서 로케이션을 이동시키게 하는거


버튼으로 바꿔주고


뷰모델에 NextLocation을 업뎃해주는 로직 만들어줌
mapLocation = location으로 가능한 이유는

location이 업뎃될때 Map도 업뎃되게 해줘서 그럼
ListView의 button action에 뷰모델에 작성한 로직을 호출해주면 끝

타이틀 텍스트가 animate되는게 거슬림
없애보자


animation에 .none을 주고 어떤 값이 변하는지에 따라서 오버라이드 할 것인지 정해줘야함
vm.mapLocation값을 넣어주고 싶지만 현재 Equatable 하지가 않음


Location Model을 Equatable하게 만들어줘야하는데

좌변 로케이션 모델의 id가 우변 로케이션모델의 id랑 같을 경우 equatable하다고 정의해줌


Location Preview cards with asymmetric Transitions


로케이션 브리뷰 뷰를 만들어보자

extension으로 imageSection이랑 titleSection 만들어줌


extension안에 버튼도 만들어주고

ui 조금씩 수정해주자(단순반복이라서 따로 적지는 않음)

다시 로케이션뷰로 돌아와서

header아래로 ZStack안에 감싸서 forEach 뷰를 만들어줌

현재 ZStack이라서 모든 locations가 다 보이는 중이라 vm.mapLocation 즉
선택된 location만 보여주도록 설정해줌


요런 에러가 발생하는데 어디를 잘못 작성한 건지 모르겠다🤔


Next버튼을 누르면 리스트뷰를 누르지 않고도 다음 장소로 이동할 수 있게 해보자

뷰모델에서 이 로직을 만들어주면 될 것 같다


현재의 index를 firstIndex를 사용해서 찾아주고,
다음 index는 그럼 현재 + 1 이 될 텐데 마지막 인덱스였다면 벗어나게되니까
guard문으로 locations.indices.contains(nextIndex)를 통해서 valid하다면 진행되도록 해준다


프리뷰 뷰에도 @EnvironmentObject 추가해주고!
작성한 nextButtonPressed 메소드를 버튼에 추가해주면 됨


Custom Map Pins

맵에 핀을 추가해보자

기존에 작성했던 Map을 살짝 바꿔줘야할 거 같다
annotationItems와 annotationContent라는 프로퍼티를 추가해줌
근데 여기서 annotationItems로 받는 타입이 RandomAccessCollection인 걸 볼 수 있다

RandomAccessCollection이 뭐지?


ForEach가 정의된 곳을 가보면 Data로 RandomAccessCollection을 넣어주는 걸 알 수 있음
알게 모르게 지금까지 사용해왔던거임


그러니까 annotationItems에는 뷰모델의 로케이션s 모델들이 들어갈거고, annotationContent에는 어떤 내용이 들어갈지 정해주면 되는데
지금은 MapMarker를 넣어줬다


default marker는 이렇게 생김
커스텀해보자

요런식으로 MapAnnotation을 작성하고 안에 뷰를 넣어주면됨

어노테이션 안에 들어갈 뷰를 구성해보자


요렇게!
frame의 크기에 여유를 조금 줘서 뷰를 띄우는 시점에 장소를 가리지 않게 만들어줌


extension으로 빼서 body 정리해주고!


Location Detail View


Location Detail View를 새로 생성해줌
여기서 탭뷰로 만들어준건 Paging뷰로 만들어주기 위해서임

.overlay로


backButton을 구성해줌


iPad, Dark Mode & Landscape

프레임크기 신경써주면됨


Final Review

profile
우주형

0개의 댓글