에러라기보단, 기술을 이해 없이 사용하여 마주한 문제 해결 정도로 봐야겠다.


문제 정의

비동기 함수를 통한 동작으로 UI반영이 안된다.

 

위치정보를 담당하는 locationManager 객체가 @Published로 정의되어 있다.

따라서 locationManager의 값이 변경이 되면 swiftUI는 자동으로 화면을 새로 그려낸다.

따라서 나는 ViewModel에서 locationManager의 속성 변경을 통해 UI를 갱신하도록 하였다.

 

locationManager 객체에는 사용자의 위치를 갱신하는 함수 locationManager(_, didUpdateLocations) 함수가 있다.

lastLocation 속성을 변경하는 동작이다. 해당 함수는 delegate 패턴으로 작성되며, 개발자는 해당 함수를 직접 호출할 수 없다.

그러면 어떻게 사용하느냐?

 

locationManager객체의 startUpdatingLocation() 함수를 통한 대리 호출만 가능하다.

해당 함수는 ViewModel에 정의하여, 여기서 호출이 일어나면 locationManager의 속성이 바뀌게 되고,

이를 SwiftUI가 자동으로 UI에 반영해 준다.

 

그리고 locationManager에는 사용자의 위치를 기반으로 주소값을 갱신하는 함수가 있다.

lastLocation을 기준으로 주소를 가져오게 된다.

 

이때 든 생각

어차피 따로 쓸 일 없는데, 호출 한 번으로 사용자 위치 갱신할 때 한 번에 주소도 갱신하면 안 됨?

 

 

내가 의도한 순서:

위치 갱신 -> 갱신한 위도, 경도를 기준으로 주소 갱신 -> 변경된 속성을 SwiftUI가 감지하여 UI 갱신

 

이런 단순한 생각으로 ViewModel에 다음과 같이 작성하였으나,

사용자 위치만 갱신될 뿐 정작 중요한 주소 레이블이 UI에 반영이 되지 않았다.

 

실제 동작

 

열심히 함수에 print()를 찍어 동작이 어떻게 이루어지다 파악해 보니 내가 작성한 것은 다음과 같이 동작했다.

delegate 패턴으로 작성된 locationManager(_, didUpdateLocation) 함수로 넘어가기 전에 비동기로 주소갱신이 이루어진다.

 

당연하게도 갱신이 되지 않은 위치로 주소를 가져오니 이전 주소가 나오고,

위도, 경도 값이 경신된 이후 함수가 종료된다.

locationManager의 대리호출과 비동기 호출의 조합으로 동작의 순서가 엉망이 돼버린 것.

 


문제 해결

짧지 않은 기간 동안의 삽질과 아직 해결되지 않은 의문점이 남아있지만 아무튼 지금은 의도된 동작대로 작동한다.

 

 

일단 위치갱신과 주소갱신을 한 번에 하겠다는 생각을 버렸다.

따라서 주소갱신함수는 locationManager의 속성을 바꾸는 것이 아닌, Sting을 반환하는 비동기 함수로 변경하였다.

* 비동기로 굳이 작성하는 이유는 주소를 반환하는 reverseDeocoderLocation() 함수가 비동기로 작성되었기 때문.

 

이어서 locationManager에 있던 주소 레이블 속성을 viewModel로 가져왔다.

이렇게 각 함수를 ViewModel에서 따로 호출할 수 있게 변경하였다.

 

이어서 View에서는 onChange를 통해 위도, 경도의 갱신이 완료되면, 주소를 가져오게 하여 동작의 순서를 정해주었다.


정리

동작을 분리한 이유는 다음 한 줄로 정리할 수 있다.

 

UI의 반영은 메인 스레드에서만 이루어진다.

 

ViewModel을 담당하는 FitcastManager@MainActor로 선언된 이유가 바로 여기서 작동하는 동작들은 모두 메인스레드에서 이루어지게 하기 위함이다.

 

 

따라서 기존의 방식대로 주소 갱신이 비동기로 동작하게 된다면 메인스레드가 아닌 새로 생성된 스레드에서 작동하게 되는 것이고, 여기서 이루어진 갱신은 UI에 반영될 수 없다고 판단하였다.

 

따라서 UI에 반영할 주소값의 갱신은 @MainActor로 선언된 ViewModel의 속성으로 두어 비동기로 작동하더라도 메인스레드에서 동작하게끔 수정해 주었다. 

 

사실 해당 문제를 해결하기까지 아직 의문점이 남은 부분이 많이 있다. 아직 aync/await의 사용법과 MainActor와 관련된 이해도가 떨어져서 그렇다고 생각한다. 해당 개념에 대해 쉽게 설명한 글이 많으므로 나중에 완벽하게 이해된다면 다시 한번 리팩토링을 통해 후기로 돌아와야겠다.

 

 

 

 

요청된 (systemMedium) 위젯 패밀리는 현재 위젯 종류에서 지원하지 않습니다.


WidgetKit을 사용하여 위젯 기능을 구현하던 중 마주한 오류다. 시뮬레이터 혹은 실기기를  통해 빌드할 때 테스트하는 환경(scheme)에서 위젯이 지원하지 않는 크기(widget Family)가 요청될 경우 발생하는 오류다. 위젯 관련 기능을 처음 만들다보니 한참을 헤맸다. 

 

struct weatherFitWidget: Widget {
    let kind: String = "WeatherFitWidget"

    var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            weatherFitWidgetEntryView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .contentMarginsDisabled()
        .configurationDisplayName("추천 옷차림")
        .description("현재 기온에 맞는 옷차림을 추천합니다.")
        .supportedFamilies([.systemSmall]) //위젯이 지원하는 크기 종류
    }
}

위젯에는 Small, Medium, Large 크기의 widgetFamily가 있다.

 

마지막 라인의 supportedFamilies() 함수를 통해 지원하는 위젯의 크기를 정해줄 수 있는데, 본인은 작은 사이즈의 위젯만 우선적으로 서비스할 생각이라 .systemSmall 하나만 지원하게 작성하였다.  *여러 개를 지원할 경우 지원하고 싶은 종류를 다 적으면 된다.


Edit Scheme 메뉴

 

작동 환경을 테스트하려고 빌드하게 될 경우, xcode에는 ".systemMedium 사이즈의 위젯을 설치하라" scheme이 작성되어있는데 해당 프로젝트는 .systemSmall 사이즈만 지원을 하니 거기서 문제가 발생한 것. 아마 처음 widgetExtension을 생성하면서 생성되는 기본 scheme으로 추정된다.

 

xcode의 메뉴 중  "Product - Scheme - Edit Scheme" 메뉴로 진입하면 테스트 환경에서 어떤 사이즈의 위젯을 설치할지 설정해 줄 수 있다. 들어가 보니 systemMedium으로 설정이 되어있어서 systemSmall로 바꾸어주니 오류가 사라졌다.

 

rsync error: some files could not be transferred (code 23) Command PhaseScriptExecution failed with a nonzero exit code

I tried to connect flutter project to my iPhone, but suddenly this error showed up after adding Google AdMob. I've already tried keychain solution(Xcode 10.2.1 Command PhaseScriptExecution failed w...

stackoverflow.com

출시된 앱의 업데이트를 위해 빌드하다가 마주한 오류다. 스택오버플로우에서 찾아보니 최근에 진행된 xcode 14.3 업데이트로 인해 발생한 오류이다. 

 

해결법은 간단하게도 해당 프로젝트의 cocoadpods 디렉터리 내에 있는 frameworks.sh 파일 속 "readlink" 명령어를 "readlink -f" 라는 옵션명을 덧붙이면 해결이 된다. 본인의 경우 "프로젝트 디렉토리/Pods/Target Support Files/Pods-프로젝트명" 위치에 "Pods-프로젝트명-frameworks.sh" 이름으로 파일이 있었다.

 

readlink 명령어에 대해 궁금해져서 찾아보았더니 유닉스 명령어로, 심볼릭 링크의 값을 출력하는 명령어이다. 7가지 옵션이 있는 것으로 나오는데, 각 옵션과 역할은 다음과 같다.

  • -f, --canonicalize : 심볼릭 링크의 원본 위치를 출력한다.
  • -n, --no-newline : 새로운 라인은 출력하지 않는다.
  • -q, --quiet, : 메시지를 출력하지 않는다.
  • -s, --silent : 대부분 에러 메시지를 출력하지 않는다.
  • -v, --verbose : 상세한 정보를 출력한다.
  • --help : 사용법을 출력한다.
  • --version : 버전 정보를 출력한다.

-f 명령어, 즉 심볼릭 링크의 원본 위치 참조를 통해 해결된 것을 보니 xcode 14.3 업데이트 항목에 cocoapods와의 연동과정에 필요한 경로 변경이 일어나 발생한 오류로 추정이 된다.

 

혹시나 해서 -f 명령어를 지우고 cocoapods를 업데이트해보았는데도 동일한 오류가 발생한 것을 보니 아직 cocoapods 측의 업데이트 대응이 진행 중인 것으로 보인다.

MKMapView를 사용하는 프로젝트를 다루던 중 시뮬레이터로 빌드 시 지도 부분이 그리드 화면으로 표시되는 문제가 발생했다. 순간 코드가 잘못되었나 생각이 들었지만, 실기기로 빌드해보면 정상 동작했다. 해당 문제는 시뮬레이터에서 발생한 문제로 개인적인 추측으로는 지도를 불러오는 데에 문제가 생긴 모양이다. 여러 가지 설정을 찾아보니 기기를 재설정하는 메뉴가 있었고 해당 메뉴로 해결할 수 있었다.

시뮬레이터 메뉴에 Device - Erase All Content and Settings를 누르면 나오는 확인창에 Erase를 선택하면 시뮬레이터가 재설정되고 정상적으로 지도를 불러오는 모습을 확인할 수 있다.

자신의 앱에 구글 애드몹을 사용한다면, 추적광고 때문에 반드시 사용자에게 해당 알림을 띄워야만 앱심사에서 통과된다. 일반적인 경우 앱의 최초실행에만 알림이 뜨면 되는 터라, 앱 출시 당시 정상 동작하는것만 확인하고서는 그뒤로는 잘 되겠거니 하고서 따로 확인을 하지 않았다.

이후 아이폰의 iOS업데이트의 영향인지 혹은 내가 변경한 코드들의 영향때문인지는 모르겠으나 일반적인 호출로는 알림이 뜨지않아 IDFA를 가져오는 과정에서 "Not Determined" 상태가 리턴되는 문제가 발생하였다. 

func requestPermission() {
    ATTrackingManager.requestTrackingAuthorization { status in
        switch status {
        case .authorized:
            // Tracking authorization dialog was shown
            // and we are authorized
            print("[googleAdmob]: Authorized")
            // Now that we are authorized we can get the IDFA
            print(ASIdentifierManager.shared().advertisingIdentifier)
        case .denied:
            // Tracking authorization dialog was
            // shown and permission is denied
            print("[googleAdmob]: Denied")
        case .notDetermined:
            // Tracking authorization dialog has not been shown
            print("[googleAdmob]: Not Determined")
        case .restricted: print("[googleAdmob]: Restricted")
        @unknown default: print("[googleAdmob]: Unknown")
        }
    }
}

앱추적 권한 상태는 크게 4가지. Authorized(허용됨), denied(거부됨), NotDetermined(결정안됨), restricted(제한됨)

주석들을 보면 알겠지만 추적권한 알림이 뜨지 않는 상태라는 걸 알수있다. 해결법은 StackOverFlow에서 손쉽게 찾을수 있었다. 

DispatchQueue.main.asyncAfter(deadline: .now()+0.2){
    self.requestPermission()
}

requestPermission() 함수를 호출하던 부분을 비동기 방식, 예약호출을 통해 실행하니 해결되었다. 현재시간에서 0.01초 단위로 늘려보았고, 0.13초부터 알림이 뜨기시작했다. 혹시 모르니 0.2초 예약호출로 작성하였고, "Not Determined"문제는 이렇게 해결되었다.

https://github.com/scenee/FloatingPanel#change-the-backdrop-alpha

 

GitHub - scenee/FloatingPanel: A clean and easy-to-use floating panel UI component for iOS

A clean and easy-to-use floating panel UI component for iOS - GitHub - scenee/FloatingPanel: A clean and easy-to-use floating panel UI component for iOS

github.com

현재 FloatingPanel을 이용하여 개발 중 만났던 삽질에 대해 글을 써본다. 

우선 FlotingPanel에 대해 간략하게 설명하자면 애플지도, 애플주식 앱을 켜면 나오는 하단의 bottomSheet이다.

이러한 기본 뷰 위에 또 다른 뷰를 띄우는데 쉽게 제작할 수 있게 라이브러리로 깃허브에 올라와있다. 관심있다면 해당링크로 들어가보자. 

FloatingPanel 라이브러리를 사용중 레이아웃을 수정하여 초기에 뜨는 뷰의 높이를 수정하려는데 아무리 수정해도 반영이 되질 않았다. 

class ViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate, FloatingPanelControllerDelegate{

    @IBOutlet var mapView: MKMapView!
    
    var fpc: FloatingPanelController!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        urlrequest(count: 1)
        
        mapView.delegate = self
        
        fpc = FloatingPanelController(delegate: self)
        
        let contentVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "ContentVC")
        fpc.set(contentViewController: contentVC)
        
        fpc.addPanel(toParent: self)

        floatingPaneldesign()
        
        fpc.show()
        fpc.layout = CustomFloatingPanelLayout()
    }

class CustomFloatingPanelLayout: FloatingPanelLayout{
    var position: FloatingPanelPosition = .bottom
    var initialState: FloatingPanelState = .tip
    
    var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
            return [
                .full: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .superview),
                .half: FloatingPanelLayoutAnchor(absoluteInset: 270.0, edge: .bottom, referenceGuide: .superview),
                .tip: FloatingPanelLayoutAnchor(absoluteInset: 110.0, edge: .bottom, referenceGuide: .superview)
            ]
        }
}

문제의 코드. CustomFloatingPanelLayout에서 initialState 변수가 첫 화면이 나올때의 높이를 조절하는 부분인데 아무리 바꾸어도 적용이 되지 않았다. 며칠동안 initialState변수만 들여다 보고있었는데 결론은 메인 뷰 컨트롤러에 있는 fpc.show()부분이었다. quick help를 읽어보니 다음과 같이 적혀있었다. 

Summary
Shows the surface view at the initial position defined by the current layout

"현재 레이아웃으로 정의된 포지션에 뷰를 띄운다." 나는 여태 라이브러리 기본값의 위치로 뷰를 띄우고 나서 레이아웃을 바꾸었으니 첫 화면이 뜰때는 무조건 기본좌표로만 띄워졌던 것이다. 문제를 인지하고나서 fpc.show()fpc.layout = Custom... 두 줄의 위치를 바꾸어주니 내가 원하던데로 첫 화면부터 원하는 높이로 띄울 수 있게 되었다. 

앱 제작 도중 만난 에러다.

여기에 막혀서 며칠이나 해맸다.. 늘 그렇듯 정말 사소한 실수였고, 모르면 그만큼 고생하게 된다는 걸 느꼈다. 

 

func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView){
        if let eventAnnotation = view.annotation as? EventAnnotation{
            print("\(eventAnnotation.title)핀이 눌렸습니다.")
            if let contentVC = storyboard?.instantiateViewController(identifier: "SearchVC") as? SearchVC{
                contentVC.updateView(eventAnnotation.title!, "", "")
            }
        }
    }

메인 컨트롤러에 있는 맵뷰의 핀을 누르면 이벤트 처리를 통해 FloatingPanel(SearchVC를 할당하였다.)에 띄워져 있는 뷰에 접근하려하였다.

func updateView(_ text1:String, _ text2:String, _ text3:String) {
        self.APGroupName.text = text1
        self.addressDong.text = text2
        self.addressDetail.text = text3
    }

이벤트 발생 시 SearchVC 클래스의 IBOutlet label의 텍스트를 수정하려는 부분에서 해당 에러가 발생했다.

에러의 원인은 검색을 통해서 금방 찾을 수 있었는데 원인은 바로 할당되지 않은 변수(nil)에 값을 넣으려니 에러가 발생한 것.. 

여기서 눈치를 챘어야했는데 바보같이 updateView()부분만 계속해서 수정했었다.

 

열심히 삽질을 하다가 문득 FloatingPanel쪽을 들여다 보게되었고, 여기서 해법을 찾았다.

나는 FloatingPanel에 띄워져 있는 뷰 컨트롤러에 접근을 해야했는데, 스토리보드의 instantiateViewController()를 통해 접근하려 했으니, 존재하지 않는 뷰에 접근하려 해서 에러가 발생했던 것이다. 

+ Recent posts