[iOS] 앱에서 메일 보내기 (feat. MessageUI)

Sehee·2024년 7월 20일

iOS 개발하기

목록 보기
8/16
post-thumbnail

시작하며,

앱에서 사용자의 피드백을 받아보고 싶다는 생각에서 시작했다
아주 간단하게 velog graphql + 앱 내 UserDefaults를 사용하여, 자체적으로 구축한 서버나 DB는 없다
오직 피드백만을 위해 서버를 구축하는 건 무리인 것 같고, 다른 방법을 모색하다가 메일을 보내는 것으로 선택하게 되었다


피드백을 받을 방법?

요구사항 분석

  1. 이 앱이 만족스러운지 여부 파악
  2. 앱을 사용하면서 느끼는 좋은점, 불편함, 개선사항 등의 의견 수집
  3. 사용자가 부담을 느끼지 않으며, 편하고 간단하게 의견 전달 (별도의 로그인, 개인정보 동의 등의 행위 X)
  4. 한 명의 사용자가 여러 번 피드백 전송 가능
  5. 만일 해당 창구로 문의가 들어오는 경우 답변 가능
  6. 사용자의 앱 이탈 방지

방법 나열

서버 없이 앱에서 피드백을 받을 수 있는 방법을 생각해보았다

웹뷰를 통해 구글폼으로 연결
요구사항 5번은 개인정보 동의 및 이메일 등을 수집하는 경우 가능하나, 그러면 요구사항 3번에 적합하지 않음

외부 메일 앱에서 이메일로 전송 (이메일 주소 복사 기능 추가)
요구사항 6번에 적합하지 않음

앱 내에서 메일 전송
요구사항 3번은 메일 앱 설정이 되어있지 않은 경우 부적합하나, 한 번 설정한 이후에는 적합함

앱스토어 리뷰로 랜딩
요구사항 4번, 6번에 적합하지 않음
(물론 앱스토어 리뷰가 달리면 좋지만,,, 본인의 목적은 이게 아님)

웹뷰를 통해 블로그로 연결 (댓글 기능 활용)
요구사항 3번에 적합하지 않음 (매번 로그인해야 함)

고로 나는 선택했다

최선의 방법은 앱 내에서 메일을 전송할 수 있도록 하는 것이다

이 경우도, 기본 메일 앱의 설정이 잘 되어있다는 것을 전제로 둔다

만일 앱에서 메일 발송을 실패한 경우를 대비하여, 외부 메일 앱에서 이메일로 전송할 수 있도록 이메일 주소 복사 기능을 추가하였다

[iOS] SwiftUI에서 클립보드 복사하기


앱에서 메일 보내기

결과부터 말하자면, 이걸 만들 것이다

UI 그리는 부분은 건너뛰고, 기능적인 부분만 설명하겠다

코드 요약

간단히 어디에 코드가 위치하는지부터 알아보자

import SwiftUI
import MessageUI

struct ContentView: View {

	// ... 변수 선언

	var body: some View {
    	VStack {
		    // ... 화면 UI
            
            // 버튼
            Button(action: {
                self.isShowingMailView.toggle()
            }, label: {
                Text("피드백 보내기")
            })
    	}
        .sheet(isPresented: $isShowingMailView) {
            self.mailView
        }
        .alert(isPresented: $showAlert) {
            Alert(title: Text("피드백"), message: Text(alertMessage), dismissButton: .default(Text("확인")))
        }
    }
    
    private var mailView: some View { ... }
}

struct MailComposeViewControllerWrapper: UIViewControllerRepresentable { ... }

이제 여기서 ContentView의 변수와 mailView, MailComposeViewControllerWrapper만 적어주면 된다

MailComposeViewControllerWrapper

사실 별 다를 건 없고, 아래 코드는 복붙해도 무방하다
(본인도 gpt가 적어준 코드 복붙한거다)

handleMailResult의 메시지는 alert로 출력되는 문구이기 때문에 원하는 문구로 바꾸어도 된다

struct MailComposeViewControllerWrapper: UIViewControllerRepresentable {
    @Binding var recipient: String
    @Binding var subject: String
    @Binding var messageBody: String
    @Binding var result: Result<MFMailComposeResult, Error>?
    @Binding var showAlert: Bool
    @Binding var alertMessage: String
    
    @Environment(\.presentationMode) var presentationMode
    
    func makeUIViewController(context: Context) -> MFMailComposeViewController {
        let mailComposer = MFMailComposeViewController()
        mailComposer.setToRecipients([recipient])
        mailComposer.setSubject(subject)
        mailComposer.setMessageBody(messageBody, isHTML: false)
        mailComposer.mailComposeDelegate = context.coordinator
        return mailComposer
    }
    
    func updateUIViewController(_ uiViewController: MFMailComposeViewController, context: Context) {
        // Update the view controller with the latest bindings
        uiViewController.setToRecipients([recipient])
        uiViewController.setSubject(subject)
        uiViewController.setMessageBody(messageBody, isHTML: false)
        
        // Handle result if it's set
        if let result = result {
            switch result {
            case .success(let result):
                handleMailResult(result)
            case .failure(let error):
                handleMailError(error)
            }
            self.result = nil // Reset result after handling
        }
    }
    
    func handleMailResult(_ result: MFMailComposeResult) {
        switch result {
        case .cancelled:
            showAlertWithMessage("메일 발송이 취소되었습니다")
        case .saved:
            showAlertWithMessage("메일이 저장되었습니다")
        case .sent:
            showAlertWithMessage("메일이 발송되었습니다\n*메일 앱에서 발송 여부를 확인해주세요")
        case .failed:
            showAlertWithMessage("메일 발송에 실패하였습니다")
        @unknown default:
            showAlertWithMessage("문제가 발생했습니다. 잠시 후 다시 시도해주세요")
        }
    }
    
    func handleMailError(_ error: Error) {
        showAlertWithMessage("Error: \(error.localizedDescription)")
    }
    
    func showAlertWithMessage(_ message: String) {
        alertMessage = message
        showAlert = true
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(parent: self)
    }
    
    class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
        var parent: MailComposeViewControllerWrapper
        
        init(parent: MailComposeViewControllerWrapper) {
            self.parent = parent
        }
        
        func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
            if let error = error {
                parent.handleMailError(error)
            } else {
                parent.handleMailResult(result)
            }
            controller.dismiss(animated: true)
        }
    }
}

ContentView 변수 선언

피드백 보내기 버튼을 클릭하면 mailView를 호출한다
이때 불러올 mailView에 ContentView에서 기재한 피드백 내용을 전달할 것이다

그러려면 피드백의 내용을 담아둘 변수가 필요하다
제목과, 받는 사람 이메일도 변수로 저장해두자

var emailToReceive: String = "test@test.com" // 고정값
@State var subject: String = ""
@State var messageBody: String = ""

메일 발송 결과를 받고, alert 창 문구를 담을 변수도 필요하다

@State private var result: Result<MFMailComposeResult, Error>? = nil
@State private var alertMessage: String = ""

메일 앱 뷰를 띄울 트리거와 alert 창을 띄울 트리거도 추가해주자

@State private var isShowingMailView: Bool = false
@State private var showAlert: Bool = false

mailView

이제 isShowingMailViewTrue인 경우 보여질 mailView를 적어보자

private var mailView: some View {
        MailComposeViewControllerWrapper(recipient: .constant(emailToReceive), subject: .constant(subject), messageBody: .constant(messageBody),  result: self.$result, showAlert: self.$showAlert, alertMessage: self.$alertMessage)
    }

실행해보기

이제 코드 적는 건 끝났다
이제 실행해보면서 안되는 부분이 있다면 체크해보자

우선 실기기로만 테스트가 가능하며, 가상 시뮬레이터로는 테스트가 불가능하다

또한, 메일 앱의 계정 설정을 먼저 체크하는 걸 추천한다
계정이 잘 연결되어있는지 확인하고, 다른 이메일 주소로 메일을 보냈을 때 성공하면 문제없다

테스트했을 때 잘 보내지지 않는 경우에는 아래 내용을 시도해보자

Info.plist에 권한 요청 추가

앱이 메일 앱에 접근하기 위해서는 NSAppleEventsUsageDescription라는 권한이 필요하다고 한다
그러나 카메라 접근 권한처럼 사용자에게 노티를 줄 수는 없다고 한다

Info.plist에 아래 항목을 추가하면 된다

<key>NSAppleEventsUsageDescription</key>
<string>피드백을 받기 위해 메일앱 접근 권한을 요청합니다</string>

마치며,

앱에서 메일을 보낼 수 있다는 것을 알았지만 이렇게 간단할 줄은 몰랐다
GPT 덕분에 간단하게 구현할 수 있었지만, 사실 메일 앱 계정 설정 확인을 안해서 조금 삽질한 시간이 있었다

이번엔 조금 한 번에 짜잔하고 만들고 계속 테스트를 하는 형태로 개발했다
그러나, 차근차근 기본 기능부터 만들고 살을 붙여나가는 형태로 개발하는 게 잘 맞는 듯 하다
특히 이렇게 외부 요인일 가능성이 있는 것들은 특히나 중요한 것 같다

profile
디자인하는 개발자

0개의 댓글