포스트

Final (24)

커뮤니티 지도 첨부

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Foundation
import MessageKit
import CoreLocation


struct Location: LocationItem {
    var location: CLLocation
    var size: CGSize
    
    init(location: CLLocation) {
        self.location = location
        self.size = CGSize(width: 240, height: 240) // 원하는 지도 크기
    }
}

지도를 위한 모델링

Message.swift 변경사항 요약

✅ 핵심 요약

기존의 텍스트, 이미지 메시지 외에 위치(Location) 메시지를 전송할 수 있도록 구조를 확장하였다.
이에 따라 location 속성, MessageKind 처리, 이니셜라이저, Firestore 연동 및 직렬화 관련 코드가 추가되었다.


1. import CoreLocation 추가

1
import CoreLocation
  • 위치 정보를 처리하기 위해 CoreLocation 프레임워크를 가져온다.

2. 프로퍼티 추가

1
var location: CLLocation?
  • 메시지가 위치 정보를 담을 수 있도록 프로퍼티를 추가하였다.

3. kind 계산 로직 확장

1
2
3
4
5
6
7
8
9
10
11
var kind: MessageKind {
    if let image = image {
        let mediaItem = ImageMediaItem(image: image)
        return .photo(mediaItem)
    } else if let location = location {
        let locationItem = Location(location: location)
        return .location(locationItem)
    } else {
        return .text(content)
    }
}
  • 위치 메시지가 존재하는 경우 .location 형태로 반환되도록 처리.

4. 이니셜라이저 2개 추가 (User / CustomUser)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
init(user: User, location: CLLocation, displayName: String) {
    sender = Sender(senderId: user.uid, displayName: displayName)
    self.location = location
    sentDate = Date()
    content = ""
    id = nil
}

init(customUser: CustomUser, location: CLLocation, displayName: String) {
    sender = Sender(senderId: customUser.uid, displayName: displayName)
    self.location = location
    sentDate = Date()
    content = ""
    id = nil
}
  • 위치 메시지를 만들 수 있도록 각각 사용자, 게스트용 초기화 메서드 추가.

5. Firestore Snapshot 초기화 로직 확장

1
2
3
4
5
else if let latitude = data["latitude"] as? CLLocationDegrees, let longitude = data["longitude"] as? CLLocationDegrees {
    location = CLLocation(latitude: latitude, longitude: longitude)
    content = ""
    downloadURL = nil
}
  • Firestore에서 위도/경도를 읽어 메시지로 변환하는 로직 추가.

6. DatabaseRepresentation 확장

1
2
3
4
else if let location = location {
    representation["latitude"] = location.coordinate.latitude
    representation["longitude"] = location.coordinate.longitude
}
  • 메시지를 저장할 때 위치 정보가 있을 경우 위도/경도를 별도 필드로 저장함.

이번 수정은 메시지 모델을 확장하여 텍스트 / 이미지 / 위치 세 가지 형태를 모두 지원할 수 있도록 한 구조적 개선이다.
이로 인해 사용자 간 위치 공유 기능을 구현할 수 있는 기반이 마련되었다.

위치 공유 기능 추가 (ChatVC)

입력 버튼 대체 및 액션시트 추가

기존 카메라 버튼 대신 paperclip 아이콘으로 교체하고, 누르면 사진/지도 선택이 가능한 액션시트를 띄우도록 수정하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Before
let cameraBarButtonItem: InputBarButtonItem = {
  ...
  button.image = UIImage(systemName: "camera")
  button.addTarget(self, action: #selector(didTapCameraButton), for: .touchUpInside)
}()

// After
let addBarButtonItem: InputBarButtonItem = {
  let button = InputBarButtonItem(type: .system)
  button.tintColor = ThemeColor.mainOrange
  button.image = UIImage(systemName: "paperclip")
  button.addTarget(self, action: #selector(presentInputActionSheet), for: .touchUpInside)
  return button
}()

2. paperclip 버튼 클릭 시 액션시트 표시

사용자 상태에 따라 분기 처리하며, 유저일 경우 사진/지도 액션시트를, 게스트일 경우 경고 메시지를 보여준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@objc private func presentInputActionSheet() {
  if user != nil {
    let actionSheet = UIAlertController(title: "유형을 선택해주세요", message: "아래에서 선택해주세요", preferredStyle: .actionSheet)

    actionSheet.addAction(UIAlertAction(title: "사진", style: .default, handler: { [weak self] _ in
      self?.didTapCameraButton()
    }))

    actionSheet.addAction(UIAlertAction(title: "지도", style: .default, handler: { [weak self] _ in
      self?.presentLocationPicker()
    }))

    actionSheet.addAction(UIAlertAction(title: "취소", style: .cancel, handler: nil))

    present(actionSheet, animated: true)
  } else if customUser != nil {
    showMessage(title: "로그인이 필요한 기능입니다.", message: "사용 할 수 없습니다.")
  }
}

3. 지도 선택 화면 전환

지도 선택을 위해 MapViewController를 모달로 띄운다. 선택된 위치는 델리게이트 메서드로 전달받는다.

1
2
3
4
5
6
7
private func presentLocationPicker() {
  let mapVC = MapViewController()
  mapVC.delegate = self
  mapVC.isLocationPicker = true
  let navController = UINavigationController(rootViewController: mapVC)
  present(navController, animated: true)
}

4. 위치 선택 후 메시지로 전송

선택된 좌표를 기반으로 Message 객체를 생성하여 Firestore에 저장한다. 이후 UI 업데이트는 리스너를 통해 처리된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
extension ChatVC: MapViewControllerDelegate {
  func didSelectLocation(_ location: CLLocationCoordinate2D) {
    guard let user = self.user else {
      print("No valid user found")
      return
    }

    let displayName = currentDisplayName

    let locationMessage = Message(
      user: user,
      location: CLLocation(latitude: location.latitude, longitude: location.longitude),
      displayName: displayName
    )

    chatFirestoreStream.save(locationMessage) { error in
      if let error = error {
        print(error)
        return
      }
      // 메시지 추가는 Firestore 리스너가 처리
    }
  }
}

MapViewController 변경사항 요약

1. delegate 프로토콜 추가

1
2
3
protocol MapViewControllerDelegate: AnyObject {
    func didSelectLocation(_ location: CLLocationCoordinate2D)
}

설명: 위치 선택 결과를 전달할 수 있도록 외부에서 처리할 delegate 프로토콜을 정의함.


2. 프로퍼티 추가

1
2
3
weak var delegate: MapViewControllerDelegate?
var selectedLocation: CLLocationCoordinate2D?
var isLocationPicker: Bool = false

설명: 지도 선택 기능에서 사용할 위치 저장 변수와 delegate 설정을 위한 프로퍼티가 추가됨. isLocationPicker는 위치 선택 모드 여부를 나타냄.


3. 버튼 UI 추가 및 제약 조건 설정

1
2
3
private let buttonStackView: UIStackView = ...
private let sendButton: UIButton = ...
private let cancelButton: UIButton = ...

설명: 선택된 위치를 전송하거나 취소할 수 있도록 Send, Cancel 버튼을 UIStackView로 구성함.


4. viewWillAppear에서 버튼 UI 활성화

1
2
3
4
5
6
override func viewWillAppear(_ animated: Bool) {
    ...
    if isLocationPicker {
        setupButtons()
    }
}

설명: 위치 선택 모드일 경우에만 버튼이 화면에 표시되도록 제어함.


5. UILongPressGestureRecognizer 등록 및 핸들러 구현

1
2
3
4
5
6
let longPressGesture = UILongPressGestureRecognizer(...)
mapView.map.addGestureRecognizer(longPressGesture)

@objc func handleLongPress(gestureRecognizer: UILongPressGestureRecognizer) {
    ...
}

설명: 지도에서 길게 눌러 좌표를 선택할 수 있도록 제스처 인식기를 추가함. 선택된 좌표는 selectedLocation에 저장되며 핀이 갱신됨.


6. 위치 선택 결과 전송 및 취소 처리

1
2
3
4
5
6
7
8
9
10
@objc func sendButtonTapped() {
    if let location = selectedLocation {
        delegate?.didSelectLocation(location)
    }
    dismiss(animated: true, completion: nil)
}

@objc func cancelButtonTapped() {
    dismiss(animated: true, completion: nil)
}

설명: 위치가 선택되었을 경우 delegate를 통해 외부에 전달하고, 현재 화면을 닫음. 취소 시에도 화면을 닫음.

커뮤니티 채널 입장 제한 추가

1. CoreLocation 도입 및 위치 기반 기능 추가

  • import CoreLocation 추가
  • 프로퍼티 추가:
1
2
3
  let locationManager: CLLocationManager = CLLocationManager()
  var userLocation: CLLocation = CLLocation()
  private var currentAddress = ""
  • CLLocationManagerDelegate 프로토콜 채택 및 관련 메서드 구현
1
2
3
4
5
6
  extension ChannelVC: CLLocationManagerDelegate {
      func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
          ...
      }
      func getAddress(coordinate: CLLocation) { ... }
  }

설명: 사용자의 현재 위치를 감지하고, 해당 위치의 행정구역(시/도)을 주소로 받아 currentAddress에 저장함. 이후 채널 입장 제어에 활용됨.


2. 채널 입장 조건 강화 (지역 일치 여부 확인)

  • didSelectRowAt 내부 로직 변경:
1
2
3
4
5
  if currentAddress == channel.name {
      // 기존 로직
  } else {
      showMessage(title: "지역 오류", message: "현재 속한 지역의 대화방만 입장가능합니다.\n현재 유저님의 속한 지역은 \(currentAddress)입니다.")
  }

설명: 채널 입장 조건으로 사용자 위치와 채널 이름(지역 이름)이 일치해야만 입장 가능하도록 제한함.


3. 위치 확인 트리거 추가

  • viewDidLoad 및 viewWillAppear에 위치 요청 메서드 추가:
1
  checkUserLocation()
  • checkUserLocation() 메서드 정의:
1
2
3
4
5
  private func checkUserLocation() {
      locationManager.delegate = self
      locationManager.requestWhenInUseAuthorization()
      locationManager.startUpdatingLocation()
  }

설명: 뷰 로딩 및 화면 재진입 시마다 사용자 위치를 확인하고 갱신하도록 설정함.


4. 툴바 버튼 제거 (일시적 또는 완전 삭제)

  • addToolBarItems() 호출 제거
  • 주석 처리된 상태로 남겨둠

설명: 채널 화면 하단 툴바(로그아웃/채널 생성 버튼)가 더 이상 호출되지 않음. UI 개선 또는 기능 분리 목적 추정.


5. ChatVC 진입 방식 변경 없음

  • 유저 타입에 따라 ChatVC로 분기하는 기존 구조 유지
  • 다만 지역 일치 여부 확인 이후에만 접근 가능하도록 위치

설명: 기존 분기 구조는 유지하면서 조건만 강화함.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.