개인 프로젝트 기록 - 4 (MessageUI)

2023. 4. 12. 15:24iOS/프로젝트

메시지 전송 테스트

음성 메시지를 보낼 방법을 고민하다가, 우선은 복잡하게 하기 보다는 메시지로 파일 전송할 수 있도록 하는 것이 좋을 것 같았습니다. 그래서 목표는 메시지를 전송할 수 있도록 테스트 하는 것입니다.

 

목표 달성

MessageUI

우선 문서를 살펴 보니 메시지 관련 두 프레임워크가 존재했습니다. Messages, MessageUI 였습니다. 살펴본 결과 앱에서 메시지를 전송하는 화면을 띄워야 하기 때문에 MessageUI를 사용하는 것이 적합하는 결론을 내렸습니다.

그 과정에서 아래의 블로그가 아주 큰 도움이 되었습니다!

https://medium.com/@DevVenusK/swift-를-이용해-sms-를-보내보자-34555582f84c

 

Swift 를 이용해 SMS 를 보내보자

영화를 예매하거나, 공연을 예매할 때 우리는 예매내역을 문자로 전송을 할 수 있어요.

medium.com

 

SwiftUI에서 ViewController 사용하기

원하는 목표를 이루기 위해 사용자가 SMS/MMS 메시지를 구성하고 전송할 수 있도록 도와주는 뷰 컨트롤러인 MFMessageComposeViewController 를 써야 했습니다.

 

그래서 아래의 아주 친절한 블로그를 참고하였습니다.

https://sarunw.com/posts/uiviewcontroller-in-swiftui/

 

How to use UIViewController in SwiftUI | Sarunw

Learn how to use UIViewController as a SwiftUI view.

sarunw.com

 

struct MessageUIView: UIViewControllerRepresentable {
    typealias UIViewControllerType = MFMessageComposeViewController
    
    /// 뷰 컨트롤러의 인스턴스를 반환한다
    func makeUIViewController(context: Context) -> MFMessageComposeViewController {
        let vc = MFMessageComposeViewController()
        
        vc.recipients = ["123456789"]   // 메시지를 받을 사람
        vc.body = "음성 메시지를 보내고 싶어요" // 메시지 내용
        
        return vc
    }
    
    /// SwiftUI 뷰의 정보로 뷰 컨트롤러의 상태를 업데이트한다
    func updateUIViewController(_ uiViewController: MFMessageComposeViewController, context: Context) {
    }
}

 

SwiftUI에서 뷰 컨트롤러를 사용하기 위해서 UIViewControllerRepresentable 이라는, UIKit의 view controller를 SwiftUI 뷰로 대응시킬 수 있게 하는 프로토콜을 사용합니다.

  • 어떤 ViewController를 사용할 건지 타입 명시합니다.
  • 필수 함수들을 구현합니다.
    • makeUIViewController 함수는 뷰 컨트롤러의 인스턴스를 반환합니다. 이 함수 내에서 필요한 설정을 할 수 있는데, 저는 메시지 수령 정보와 텍스트 내용을 넣어 주었습니다.
    • updateUIViewController  SwiftUI 뷰의 정보로 뷰 컨트롤러의 상태를 업데이트합니다. 저는 딱히 필요하지 않은 것 같아 스킵하였습니다.

 

SwiftUI에서 delegate 사용하기

메시지 화면은 잘 떴는데, 취소 버튼을 눌러도 아무 반응이 없더라구요! 그래서 취소 / 전송 성공 / 오류 등등의 처리를 하기 위한 MFMessageComposeViewControllerDelegate 를 사용해야 했습니다.

아 그리고 SwiftUI에서 모달을 닫기 위해서 UIKit에서 사용되는 dismiss 대신 전달받은 isPresented 값을 바꾸는 방식을 사용하였습니다.

 

struct MessageUIView: UIViewControllerRepresentable {
    @Binding var isPresented: Bool
    
    // ...

    typealias Coordinator = MessageCoordinator
    
    func makeUIViewController(context: Context) -> MFMessageComposeViewController {
        // ...
        
        vc.messageComposeDelegate = context.coordinator
        
        return vc
    }
    
    // ...

    /// makeCoordinator(): 뷰 컨트롤러의 변화를 SwiftUI에 알리기 위해 사용하는 커스텀 인스턴스
    func makeCoordinator() -> Coordinator {
        return MessageCoordinator(self)
    }
}

 

  • @Binding 값으로 모달이 보여질지 여부를 나타내는 isPresented 변수를 받습니다
  • makeUIViewController 에서 뷰 컨트롤러를 설정할 때, messageComposeDelegate context coordinator로 설정합니다.
  • 뷰 컨트롤러의 변화를 SwiftUI에 알리기 위해 사용하는 커스텀 인스턴스인 makeCoordinator()는 우리가 불러줄 필요 없이 자동으로 호출이 되며 따라서 위에서 context.coordiantor 로 접근할 수 있게 됩니다.

 

이제 실제 Coordinator 를 구현합니다.

 

/// 메시지 전송 처리를 하기 위한 Coordinator
class MessageCoordinator: NSObject, MFMessageComposeViewControllerDelegate {
    
    var parent: MessageUIView

    init(_ parent: MessageUIView) {
        self.parent = parent
    }
    
    // TODO: 메시지 전송 처리
    func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
        switch result {
            case .cancelled:
                print("cancelled")
                parent.isPresented = false
            case .sent:
                print("sent message:", controller.body ?? "")
                parent.isPresented = false
            case .failed:
                print("failed")
                parent.isPresented = false
            @unknown default:
                print("unkown Error")
                parent.isPresented = false
            }
    }
}

 

  • parent MessageUIView 를 받습니다. 이는 parent isPresent 값에 접근하기 위함입니다.
  • messageComposeViewController  result 값에 따라 알맞은 처리를 해줍니다. 우선은 어떤 경우든 다 모달을 닫도록 구현했습니다.

 

실제 메시지 화면 띄워보기

import SwiftUI
import MessageUI

struct TestMessageView: View {
    @State var isPresented = false
    
    var body: some View {
        Button("메시지를 보내고 싶다") {
            /// canSendText: 지금 디바이스가 메시지를 보낼 수 있는 상태인지 Bool 값 리턴
            guard MFMessageComposeViewController.canSendText() else {
                    print("메시지를 보낼 수 없습니다.")
                    return
            }
            isPresented = true
        }
        .sheet(isPresented: $isPresented) {
            MessageUIView(isPresented: $isPresented)
        }
    }
}

 

  • isPresented 값을 선언하고 MessageUIView에 Binding으로 넘겨줍니다. 그리고 그 값에 따라 모달이 닫힘 / 열림이 결정됩니다.
  • 메시지를 보내기 전에 MFMessageComposeViewController.canSendText 를 통해 메시지를 보낼 수 있는 상태인지 확인해야 합니다.
    • 만약 메시지를 보낼 수 없는 경우엔  MFMessageComposeViewControllerTextMessageAvailabilityDidChange notification의 옵저버를 등록해서 메시지를 보낼 수 있는 상태로 변할 때 알 수 있다고 합니다. 그런데 Notification은 아직 익숙치 않아 나중에 해 봐야겠습니다..
  • 버튼을 누르면 메시지 수령 번호와 텍스트가 적힌 아까 설정한 MFMessageComposeViewController, 즉 MessageUIView 가 화면에 띄워집니다.

결과 화면

 

아! 참고로 MessageUI는 시뮬레이터로는 확인이 되지 않습니다. 꼭 실제 기기로 확인하세요.

 

남아있는 질문들

  • 사용하지 않은 updateUIViewController의 역할이 정확히 무엇인지?
  • 메시지 전송 결과를 제대로 처리하는 법?
  • Notification을 통해 메시지 전송 가능 상태를 아는 법?

대답한 질문들

없습니다.. 반성해야겠습니다. 한번 날을 잡아서 대답 못한 질문들을 대답해야겠네요.