본문 바로가기

MOBILE/ios

[iOS] 화면전환 method present(_:animated:completion:) 의 비동기식 처리

ios에서 화면을 전환하는 방식은 여러가지인데 그 중에서 가장 classic한 방법은 바로 present 메서드를 사용하는 것이다.

화면 전환 스타일 중 "modally" 방식으로 화면을 전환해주며 view controller에 의한 화면 전환이다.

"modal"이라는 것은 화면 외에 나머지 영역이 비활성화되어 사용자는 상호작용을 할 수 없는 방식을 말한다.

(alert 중에서 .alert 형태의 사용자 알림창이 modal 형식이다.) 

이때 중요한 것은 파라미터 중 completion 인데, 이는 화면 전환이 완전이 끝난 후에 실행될 코드를 보장해준다.

 

apple developer document의 설명

 

단순하게 present 메서드 뒤에 적히는 코드는 실행이 될지 안될지 보장해주지 않는다. 왜냐하면 화면 전환은 비동기로 이루어지기 때문이다.

따라서 화면도 전환하고 그 뒤의 코드도 동시에 실행되기 때문에 화면 전환 후에 실행됨이 보장되지 않는다.

하지만 무조건 실행해야 하는 코드를 completion에 클로져로 작성하게 되면 무조건 화면 전환이 끝난 후 실행됨을 보장받을 수 있다.

 

ios에서 메세지 창을 띄워주는 alert는 UIAlertController() 객체에서 담당하는데, 이는 일종의 뷰 컨트롤러이기 때문에 view controller 에 의한 화면 전환 방식을 사용해야 한다.

따라서 alert를 띄우기 위해서는 present method를 사용해야 한다.

 

다음과 같이 action sheet를 활용하여 UIImagePickerController의 SourceType을 정하는 코드를 작성한다고 하자

 

 

 

코드는 대충 다음과 같을 것이다.

버튼 선택에 따른 결과는 sourceType 이라는 int 형 변수에다가 저장했다.

 

    func chooseSourceType()
    {
        let alert = UIAlertController(title: nil, message: "이미지를 가져올 곳을 선택하세요", preferredStyle: .actionSheet)
        let action_cam = UIAlertAction(title: "카메라", style: .default) { (_) in
            self.sourceType = 0
        }
        let action_lib = UIAlertAction(title: "사진 라이브러리", style: .default) { (_) in
            self.sourceType = 1
        }
        let action_saved = UIAlertAction(title: "저장앨범", style: .default) { (_) in
            self.sourceType = 2
        }
        
        alert.addAction(action_cam)
        alert.addAction(action_lib)
        alert.addAction(action_saved)
        
        self.present(alert, animated: true, completion: nil)
    }

 

그럼 여기에서 sourceType에 따른 imagepicker controller 코드는 어디에 작성해야 할까?

 

 

1. present method의 completion

present method는 화면 전환이 끝난 후에 코드가 실행됨을 보장해주기 때문에 코드 부분을 따로 함수로 작성해서 completion에 함수 인자로 넘겨주는 아이디어이다. 따라서 다음과 같은 형태의 코드가 작성될 것이다.

 

    func loadImg()
    {
        let picker = UIImagePickerController()
        
        picker.delegate = self
        picker.allowsEditing = true
        
        switch self.sourceType
        {
        case 0:
            picker.sourceType = .camera
        case 1:
            picker.sourceType = .photoLibrary
        default:
            picker.sourceType = .savedPhotosAlbum
        }
        NSLog("값: \(self.sourceType)")
        self.present(picker, animated: true, completion: nil)
    }
    
    func chooseSourceType()
    {
        let alert = UIAlertController(title: nil, message: "이미지를 가져올 곳을 선택하세요", preferredStyle: .actionSheet)
        let action_cam = UIAlertAction(title: "카메라", style: .default) { (_) in
            self.sourceType = 0)
        }
        let action_lib = UIAlertAction(title: "사진 라이브러리", style: .default) { (_) in
            self.sourceType = 1
        }
        let action_saved = UIAlertAction(title: "저장앨범", style: .default) { (_) in
            self.sourceType = 2
        }
        
        alert.addAction(action_cam)
        alert.addAction(action_lib)
        alert.addAction(action_saved)
        
        self.present(alert, animated: true, completion: loadImg)
    }

 

결론부터 말하자면 이 방식은 틀렸다.

가정대로라면 NSLog에 찍히는 내용은 사용자가 버튼을 선택한 이후에 나와야 하는데, 실제로는 action sheet가 보임과 동시에 콘솔에 값이 찍힌다.

 

이는 present method의 completion은 화면 전환이 끝났을 때 실행하는 코드를 보장하는 것이지

사용자의 선택이 끝났을 때 실행하는 것을 보장하는 것이 아니기 때문이다.

present와 같은 화면 전환 메서드는 비동기로 이루어진다. 따라서 화면 전환과는 상관없이 코드는 계속 진행되는 것이다.

 

따라서 이런식으로 "이미 alert Present 된게 있는데 왜 또 present를 하라는 거임?" 라고 말하는 경고창이 뜬다.

 

xcode 콘솔에 찍히는 오류

 

 

2. method의 분리

제 3의 메서드를 만들어서 각자 실행하면 되지 않을까? 하는 아이디어이다.

이 또한 결론부터 말하자면 틀렸다.

 

가정대로라면 코드는 다음과 같이 진행된다.

 

   @IBAction func btnCamera(_ sender: Any) {
        chooseSourceType()
        loadImg()
    }
    
    func loadImg()
    {
        let picker = UIImagePickerController()
        
        picker.delegate = self
        picker.allowsEditing = true
        
        switch self.sourceType
        {
        case 0:
            picker.sourceType = .camera
        case 1:
            picker.sourceType = .photoLibrary
        default:
            picker.sourceType = .savedPhotosAlbum
        }
        NSLog("값: \(self.sourceType)")
        self.present(picker, animated: true, completion: nil)
    }
    
    func chooseSourceType()
    {
        let alert = UIAlertController(title: nil, message: "이미지를 가져올 곳을 선택하세요", preferredStyle: .actionSheet)
        let action_cam = UIAlertAction(title: "카메라", style: .default) { (_) in
            self.sourceType = 0)
        }
        let action_lib = UIAlertAction(title: "사진 라이브러리", style: .default) { (_) in
            self.sourceType = 1
        }
        let action_saved = UIAlertAction(title: "저장앨범", style: .default) { (_) in
            self.sourceType = 2
        }
        
        alert.addAction(action_cam)
        alert.addAction(action_lib)
        alert.addAction(action_saved)
        
        self.present(alert, animated: true, completion: nil)
    }

 

이런식으로 btnCamera 라는 버튼을 만들고 그 안에서 action sheet, image picker controller를 각각 다른 메서드로 구현하여 실행 순서를 조절하자는 아이디어이다.

하지만 이 또한 1번과 같은 이유로 NSLog에 찍히는 내용은 사용자가 버튼을 선택한 이후에 나와야 하는데, 실제로는 action sheet가 보임과 동시에 콘솔에 값이 찍힌다.

present는 화면을 전환하고 나면 종료된다. 게다가 비동기 메서드이기 때문에 present method를 실행하면서 화면 전환을 준비하는 동시에!

chooseSourceType() 메서드가 끝났으므로 loadImg() 메서드를 실행시킨다. 따라서 동작하지 않는다.

 

그럼 도대체 어디에 넣어야 하는 것인가?

 

 

3.  UIAlertAction의 handler

정답은 action 파라미터의 handler에 넣는 것이다.

 

apple developer document의 설명

 

handler에 넣게 되면 버튼이 눌렸을 때의 동작할 action code를 보장받는다.

따라서 다음과 같은 코드가 최종 결론이 된다.

핸들러에 클로저 형태로 작성해주었다.

 

    func chooseSourceType()
    {
        let alert = UIAlertController(title: nil, message: "이미지를 가져올 곳을 선택하세요", preferredStyle: .actionSheet)
        let action_cam = UIAlertAction(title: "카메라", style: .default) { (_) in
            self.sourceType = 0
            self.loadImg()
        }
        let action_lib = UIAlertAction(title: "사진 라이브러리", style: .default) { (_) in
            self.sourceType = 1
            self.loadImg()
        }
        let action_saved = UIAlertAction(title: "저장앨범", style: .default) { (_) in
            self.sourceType = 2
            self.loadImg()
        }
        alert.addAction(action_cam)
        alert.addAction(action_lib)
        alert.addAction(action_saved)
        
        self.present(alert, animated: true, completion: nil)
    }

 

화면 전환이 비동기로 이루어지고, 그에 따른 처리 또한 다양하게 해줘야 한다는 사실을 잊지 말자